[ADF-1986] Content matadata editing phase II. (#2796)

* Aspects collection

* Fetch only those metadata aspects which are defined in the application config

* Aspect property filtering first round

* Addig wildcard support for preset, default preset fallback to wildcard, and logging

* Add white list service

* Renaming services

* ContentMetadataService and CardViewItemDispatcherComponent update

* Observables... Observables everywhere...

* Propers CardViewAspect

* Defining more interfaces

* Dummy data and expansions panels

* Fix build attempt & proper panel expansion

* Folder restructuring

* Add different type mappings

* Restructuring Card view component

* Fix build

* More ECM types supported

* Validators first phase, extraction of interfaces, world domination preparation

* Validators phase II.

* Integer and float validators

* Hide empty text items and validation message foundation

* Validation error messages for text item view, small style changes

* Update date item

* bool item component

* Datetimepicker npm module

* Datetime model

* Add mapping for datetime

* Small fixes

* Passing down preset

* Adding forgotten package.json entry

* Adding some tests for wrapper card component

* content-metadata.component tests

* Covering some edge cases, adding some tests

* Fix cardview component's test

* Add datetimepicker to demoshell

* card view component show empty values by default

* displayEmpty dependency injection

* remove table like design from cardview

* Using alfresco-js-api instead of spike

* Remove spike folder and contents

* Fix test

* Cardview updated docs

* Content metadata md

* Fix review issues

* Fix the packagr issue
This commit is contained in:
Popovics András 2018-01-11 12:31:22 +00:00 committed by Eugenio Romano
parent 994041fb23
commit 783f7f0497
106 changed files with 3816 additions and 724 deletions

View File

@ -60,6 +60,10 @@
],
"private": true,
"dependencies": {
"@alfresco/adf-content-services": "2.0.0",
"@alfresco/adf-core": "2.0.0",
"@alfresco/adf-insights": "2.0.1",
"@alfresco/adf-process-services": "2.0.0",
"@angular/animations": "5.1.1",
"@angular/cdk": "5.0.1",
"@angular/common": "5.1.1",
@ -72,12 +76,10 @@
"@angular/platform-browser": "5.1.1",
"@angular/platform-browser-dynamic": "5.1.1",
"@angular/router": "5.1.1",
"@mat-datetimepicker/core": "^1.0.1",
"@mat-datetimepicker/moment": "^1.0.1",
"@ngx-translate/core": "8.0.0",
"alfresco-js-api": "2.0.0",
"@alfresco/adf-content-services": "2.0.0",
"@alfresco/adf-process-services": "2.0.0",
"@alfresco/adf-core": "2.0.0",
"@alfresco/adf-insights": "2.0.1",
"chart.js": "2.5.0",
"classlist.js": "1.1.20150312",
"core-js": "2.4.1",

View File

@ -359,5 +359,12 @@
}
]
}
},
"content-metadata": {
"presets": {
"default": {
"exif:exif": "*"
}
}
}
}

View File

@ -38,10 +38,11 @@ Displays a configurable property list renderer.
### Properties
| Name | Type | Description |
| --- | --- | --- |
| properties | [CardViewItem](#cardviewitem)[] | (**required**) The custom view to render |
| editable | boolean | If the component editable or not |
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| properties | [CardViewItem](#cardviewitem)[] | - | (**required**) The custom view to render |
| editable | boolean | - | If the component editable or not |
| displayEmpty | boolean | true | Whether to show empty properties in non-editable mode |
## Details
@ -75,11 +76,15 @@ export interface CardViewItem {
}
```
At the moment three models are defined out of the box:
At the moment these are the models supported:
- **CardViewTextItemModel** - *for text items*
- **CardViewMapItemModel** - *for map items*
- **CardViewDateItemModel** - *for date items*
- **CardViewDatetimeItemModel** - *for datetime items*
- **CardViewBoolItemModel** - *for bool items (checkbox)*
- **CardViewIntItemModel** - *for integers items*
- **CardViewFloatItemModel** - *for float items*
Each of them extends the abstract CardViewBaseItemModel class to add some custom functionality to the basic behaviour.
@ -100,13 +105,39 @@ Each of them extends the abstract CardViewBaseItemModel class to add some custom
clickable: true
}),
new CardViewDateItemModel({
label: 'Birth of date',
label: 'Date of birth',
value: someDate,
key: 'birth-of-date',
key: 'date-of-birth',
default: new Date(),
format: '<any format that momentjs accepts>',
editable: true
}),
new CardViewDatetimeItemModel({
label: 'Datetime of birth',
value: someDatetime,
key: 'datetime-of-birth',
default: new Date(),
format: '<any format that momentjs accepts>',
editable: true
}),
new CardViewBoolItemModel({
label: 'Vulcanian',
value: true,
key: 'vulcanian',
default: false
}),
new CardViewIntItemModel({
label: 'Intelligence',
value: 213,
key: 'intelligence',
default: 1
}),
new CardViewFloatItemModel({
label: 'Mental stability',
value: 9.9,
key: 'mental-stability',
default: 0.0
}),
...
]
```
@ -162,7 +193,7 @@ const mapItemProperty = new CardViewMapItemModel(options);
| --- | --- | --- | --- |
| label* | string | --- | The label to render |
| value* | Map | --- | A map that contains the key value pairs |
| key* | string | --- | the key of the property. Have an important role when editing the property. |
| key* | string | --- | the key of the property. |
| default | any | --- | The default value to render in case the value is empty |
| displayValue* | string | --- | The value to render |
| clickable | boolean | false | Whether the property clickable or not |
@ -179,12 +210,81 @@ const dateItemProperty = new CardViewDateItemModel(options);
| --- | --- | --- | --- |
| label* | string | --- | The label to render |
| value* | any | --- | The original value |
| key* | string | --- | the key of the property. Have an important role when editing the property. |
| key* | string | --- | the key of the property. |
| default | any | --- | The default value to render in case the value is empty |
| displayValue* | string | --- | The value to render |
| displayValue* | any | --- | The value to render |
| editable | boolean | false | Whether the property editable or not |
| format | boolean | "MMM DD YYYY" | any format that momentjs accepts |
#### Card Datetime Item
CardViewDatetimeItemModel is a property type for datetime properties.
```js
const datetimeItemProperty = new CardViewDatetimeItemModel(options);
```
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| label* | string | --- | The label to render |
| value* | any | --- | The original value |
| key* | string | --- | the key of the property. |
| default | any | any | The default value to render in case the value is empty |
| displayValue* | string | --- | The value to render |
| editable | boolean | false | Whether the property editable or not |
| format | boolean | "MMM DD YYYY HH:mm" | any format that momentjs accepts |
#### Card Bool Item
CardViewBoolItemModel is a property type for boolean properties.
```js
const boolItemProperty = new CardViewBoolItemModel(options);
```
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| label* | string | --- | The label to render |
| value* | boolean | --- | The original value |
| key* | string | --- | the key of the property. |
| default | boolean | false | The default value to render in case the value is empty |
| displayValue* | boolean | --- | The value to render |
| editable | boolean | false | Whether the property editable or not |
#### Card Int Item
CardViewIntItemModel is a property type for integer properties.
```js
const intItemProperty = new CardViewIntItemModel(options);
```
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| label* | string | --- | The label to render |
| value* | integer | --- | The original value |
| key* | string | --- | the key of the property. |
| default | integer | --- | The default value to render in case the value is empty |
| displayValue* | integer | --- | The value to render |
| editable | boolean | false | Whether the property editable or not |
#### Card Float Item
CardViewFloatItemModel is a property type for float properties.
```js
const floatItemProperty = new CardViewFloatItemModel(options);
```
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| label* | string | --- | The label to render |
| value* | float | --- | The original value |
| key* | string | --- | the key of the property. |
| default | float | --- | The default value to render in case the value is empty |
| displayValue* | float | --- | The value to render |
| editable | boolean | false | Whether the property editable or not |
### Defining your custom card Item
Card item components are loaded dynamically, which makes you able to define your own custom component for the custom card item type.

View File

@ -0,0 +1,113 @@
# Content Metadata Card component
<!-- markdown-toc start - Don't edit this section. npm run toc to generate it-->
<!-- toc -->
<!-- tocstop -->
<!-- markdown-toc end -->
Allows a user to display and edit metadata related to a node.
<img src="docassets/images/ContentMetadata.png" width="325">
## Properties
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| node | MinimalNodeEntryEntity | - | (**required**) The node entity to fetch metadata about |
| displayEmpty | boolean | false | Display empty values in card view or not |
| preset | string | "*" | The metadata preset's name, which defines aspects and their properties |
## Basic Usage
The component shows metadata related to the given node. The component uses the card view component to render the properties of metadata aspects.
The different aspects and their properties to be shown can be configured as application config preset, for details about it see the related section below. By default the component only shows the basic properties of the node. Clicking on the pencil icon at the bottom, renders the underlying card view component in edit mode enabling to edit and update the metadata properties.
```html
<adf-content-metadata-card
[displayEmpty]="false"
[preset]="'*'"
[node]="node">
</adf-content-metadata-card>
```
## Application config presets
In the application config file you can define different presets for the metadata component or override the default preset. The **default** preset is "*" if not set, meaning the component will display every aspects and properties of the nodes without filtering. One can think about presets as **whitelist filters** for the content metadata component.
Beside the default preset you can define as many presets as you want, if you'd like to use different metadata components with different presets.
To understand presets better, you can have a look at on the following different example configurations.
### Mimicking the default "default" preset
If you don't have any preset configured manually in you application config, this would be equivalent as if you had the application config as defined below:
```json
...
"content-metadata": {
"presets": {
"default": "*"
}
}
...
```
### Whitelisting only a few aspects in the default preset
If you want to restrict to only a few aspects (e.g.: exif, your-custom-aspect), you have to use the name of that particular aspect to be able to whitelist it. In case of exif aspect this is "exif:exif".
```json
...
"content-metadata": {
"presets": {
"default": {
"custom:aspect": "*",
"exif:exif": "*"
}
}
}
...
```
### Whitelisting only a few properties of a few aspects in the default preset
If you want to filter more, you can do this on property level also. For this, you have to list the names of whitelisted aspect properties in an array of strings. Again, for identifying a property, you have to use its name.
```json
...
"content-metadata": {
"presets": {
"default": {
"custom:aspect": "*",
"exif:exif": [ "exif:width", "exif:height"]
}
}
}
...
```
### Whitelisting only a few properties of a few aspects in a custom preset
And finally, you can create any custom aspect following the same rules.
```json
...
"content-metadata": {
"presets": {
"default": "*",
"kitten-images": {
"custom:aspect": "*",
"exif:exif": [ "exif:width", "exif:height"]
}
}
}
...
```
## What happens when there is a whitelisted aspect in the config but the given node doesn't relate to that aspect
Nothing, this aspect (as it is not related to the node) will be simply ignored and not be displayed. The aspects to be displayed are calculated as an intersection of the preset's aspects and the aspects related to the node.
<!-- Don't edit the See also section. Edit seeAlsoGraph.json and run config/generateSeeAlso.js -->
<!-- seealso start -->
<!-- seealso end -->

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View File

@ -0,0 +1,28 @@
<mat-card *ngIf="node">
<mat-card-content>
<adf-content-metadata
[node]="node"
[displayEmpty]="displayEmpty"
[editable]="editable"
[expanded]="expanded"
[preset]="preset">
</adf-content-metadata>
</mat-card-content>
<mat-card-footer class="adf-content-metadata-card-footer" fxLayout="row" fxLayoutAlign="space-between stretch">
<div>
<button mat-icon-button (click)="toggleEdit()" data-automation-id="mata-data-card-toggle-edit">
<mat-icon>mode_edit</mat-icon>
</button>
</div>
<button mat-button (click)="toggleExpanded()" data-automation-id="mata-data-card-toggle-expand">
<ng-container *ngIf="!expanded">
<span data-automation-id="mata-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="mata-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

@ -3,9 +3,18 @@
$background: map-get($theme, background);
$foreground: map-get($theme, foreground);
.adf-viewer-default-sidebar {
.adf-content-metadata-card {
&-card-footer.mat-card-footer {
.mat-card {
padding: 0;
.mat-card-content {
margin-bottom: 0;
}
}
&-footer.mat-card-footer {
margin: 0;
padding: 8px 12px;
border-top: 1px solid mat-color($foreground, text, 0.07);

View File

@ -0,0 +1,172 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* 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.
*/
/*tslint:disable: ban*/
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { MinimalNodeEntryEntity } from 'alfresco-js-api';
import { ContentMetadataCardComponent } from './content-metadata-card.component';
import { ContentMetadataComponent } from '../content-metadata/content-metadata.component';
import { MatExpansionModule, MatCardModule, MatButtonModule, MatIconModule } from '@angular/material';
import { ContentMetadataService } from '../../services/content-metadata.service';
import { BasicPropertiesService } from '../../services/basic-properties.service';
import { PropertyDescriptorLoaderService } from '../../services/properties-loader.service';
import { PropertyDescriptorsService } from '../../services/property-descriptors.service';
import { AspectWhiteListService } from '../../services/aspect-whitelist.service';
import { AlfrescoApiService } from '@alfresco/adf-core';
describe('ContentMetadataCardComponent', () => {
let component: ContentMetadataCardComponent,
fixture: ComponentFixture<ContentMetadataCardComponent>,
node: MinimalNodeEntryEntity,
preset = 'custom-preset';
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
MatExpansionModule,
MatCardModule,
MatButtonModule,
MatIconModule
],
declarations: [
ContentMetadataCardComponent,
ContentMetadataComponent
],
providers: [
ContentMetadataService,
BasicPropertiesService,
PropertyDescriptorLoaderService,
PropertyDescriptorsService,
AspectWhiteListService,
AlfrescoApiService
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ContentMetadataCardComponent);
component = fixture.componentInstance;
node = <MinimalNodeEntryEntity> {
aspectNames: [],
content: {},
properties: {},
createdByUser: {},
modifiedByUser: {}
};
component.node = node;
component.preset = preset;
fixture.detectChanges();
});
afterEach(() => {
fixture.destroy();
TestBed.resetTestingModule();
});
it('should have displayEmpty input param as false by default', () => {
expect(component.displayEmpty).toBe(false);
});
it('should pass through the node to the underlying component', () => {
const contentMetadataComponent = fixture.debugElement.query(By.directive(ContentMetadataComponent)).componentInstance;
expect(contentMetadataComponent.node).toBe(node);
});
it('should pass through the preset to the underlying component', () => {
const contentMetadataComponent = fixture.debugElement.query(By.directive(ContentMetadataComponent)).componentInstance;
expect(contentMetadataComponent.preset).toBe(preset);
});
it('should pass through the preset to the underlying component', () => {
component.displayEmpty = true;
fixture.detectChanges();
const contentMetadataComponent = fixture.debugElement.query(By.directive(ContentMetadataComponent)).componentInstance;
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 expanded to the underlying component', () => {
component.expanded = true;
fixture.detectChanges();
const contentMetadataComponent = fixture.debugElement.query(By.directive(ContentMetadataComponent)).componentInstance;
expect(contentMetadataComponent.expanded).toBe(true);
});
it('should not show anything if node is not present', () => {
component.node = undefined;
fixture.detectChanges();
const contentMetadataComponent = fixture.debugElement.query(By.directive(ContentMetadataComponent));
expect(contentMetadataComponent).toBeNull();
});
it('should toggle editable by clicking on the button', () => {
component.editable = true;
fixture.detectChanges();
const button = fixture.debugElement.query(By.css('[data-automation-id="mata-data-card-toggle-edit"]'));
button.triggerEventHandler('click', {});
fixture.detectChanges();
expect(component.editable).toBe(false);
});
it('should toggle expanded by clicking on the button', () => {
component.expanded = true;
fixture.detectChanges();
const button = fixture.debugElement.query(By.css('[data-automation-id="mata-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="mata-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="mata-data-card-toggle-expand-label"]'));
expect(buttonLabel.nativeElement.innerText.trim()).toBe('ADF_VIEWER.SIDEBAR.METADATA.LESS_INFORMATION');
});
});

View File

@ -18,19 +18,23 @@
import { Component, Input, ViewEncapsulation } from '@angular/core';
import { MinimalNodeEntryEntity } from 'alfresco-js-api';
const PROPERTY_COUNTER_WHILE_COLLAPSED = 5;
@Component({
selector: 'adf-content-metadata-card',
templateUrl: './content-metadata-card.component.html',
styleUrls: ['./content-metadata-card.component.scss'],
encapsulation: ViewEncapsulation.None,
host: { 'class': 'adf-viewer-default-sidebar' }
host: { 'class': 'adf-content-metadata-card' }
})
export class ContentMetadataCardComponent {
@Input()
node: MinimalNodeEntryEntity;
@Input()
displayEmpty: boolean = false;
@Input()
preset: string;
editable: boolean = false;
expanded: boolean = false;
@ -41,8 +45,4 @@ export class ContentMetadataCardComponent {
toggleExpanded(): void {
this.expanded = !this.expanded;
}
get maxPropertiesToShow(): number {
return this.expanded ? Infinity : PROPERTY_COUNTER_WHILE_COLLAPSED;
}
}

View File

@ -0,0 +1,43 @@
<div class="adf-metadata-properties">
<mat-accordion displayMode="flat">
<mat-expansion-panel [expanded]="!expanded" [hideToggle]="!expanded">
<mat-expansion-panel-header>
<mat-panel-title>
{{ 'CORE.METADATA.BASIC.HEADER' | translate }}
</mat-panel-title>
</mat-expansion-panel-header>
<adf-card-view
class="adf-metadata-properties-basic"
data-automation-id="adf-metadata-properties-basic"
[properties]="basicProperties$ | async"
[editable]="editable"
[displayEmpty]="displayEmpty">
</adf-card-view>
</mat-expansion-panel>
<ng-container *ngIf="expanded">
<ng-container *ngIf="aspects$ | async; else loading; let aspects">
<div *ngFor="let aspect of aspects" class="adf-metadata-properties-aspect">
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>
{{aspect.title}}
</mat-panel-title>
</mat-expansion-panel-header>
<adf-card-view
class="adf-node-aspect-properties"
[properties]="aspect.properties"
[editable]="editable"
[displayEmpty]="displayEmpty">
</adf-card-view>
</mat-expansion-panel>
</div>
</ng-container>
<ng-template #loading>
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</ng-template>
</ng-container>
</mat-accordion>
</div>

View File

@ -0,0 +1,9 @@
@mixin adf-content-metadata-theme($theme) {
.adf {
&-metadata-properties {
.mat-expansion-panel:not([class*=mat-elevation-z]) {
box-shadow: none;
}
}
}
}

View File

@ -0,0 +1,241 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* 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.
*/
/*tslint:disable: ban*/
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { SimpleChange } from '@angular/core';
import { By } from '@angular/platform-browser';
import { MinimalNodeEntryEntity } from 'alfresco-js-api';
import { ContentMetadataComponent } from './content-metadata.component';
import { MatExpansionModule, MatButtonModule, MatIconModule } from '@angular/material';
import { ContentMetadataService } from '../../services/content-metadata.service';
import { BasicPropertiesService } from '../../services/basic-properties.service';
import { PropertyDescriptorLoaderService } from '../../services/properties-loader.service';
import { PropertyDescriptorsService } from '../../services/property-descriptors.service';
import { AspectWhiteListService } from '../../services/aspect-whitelist.service';
import { AlfrescoApiService } from '@alfresco/adf-core';
import { CardViewBaseItemModel, CardViewComponent, CardViewUpdateService, NodesApiService, LogService } from '@alfresco/adf-core';
import { ErrorObservable } from 'rxjs/observable/ErrorObservable';
import { Observable } from 'rxjs/Observable';
describe('ContentMetadataComponent', () => {
let component: ContentMetadataComponent,
fixture: ComponentFixture<ContentMetadataComponent>,
node: MinimalNodeEntryEntity,
preset = 'custom-preset';
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
MatExpansionModule,
MatButtonModule,
MatIconModule
],
declarations: [
ContentMetadataComponent
],
providers: [
ContentMetadataService,
BasicPropertiesService,
PropertyDescriptorLoaderService,
PropertyDescriptorsService,
AspectWhiteListService,
AlfrescoApiService,
NodesApiService,
{ provide: LogService, useValue: { error: jasmine.createSpy('error') } }
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ContentMetadataComponent);
component = fixture.componentInstance;
node = <MinimalNodeEntryEntity> {
id: 'node-id',
aspectNames: [],
content: {},
properties: {},
createdByUser: {},
modifiedByUser: {}
};
component.node = node;
component.preset = preset;
fixture.detectChanges();
});
afterEach(() => {
fixture.destroy();
TestBed.resetTestingModule();
});
describe('Default input param values', () => {
it('should have editable input param as false by default', () => {
expect(component.editable).toBe(false);
});
it('should have displayEmpty input param as false by default', () => {
expect(component.displayEmpty).toBe(false);
});
it('should have expanded input param as false by default', () => {
expect(component.expanded).toBe(false);
});
});
describe('Saving', () => {
it('should save the node on itemUpdate', () => {
const property = <CardViewBaseItemModel> { key: 'property-key', value: 'original-value' },
updateService: CardViewUpdateService = fixture.debugElement.injector.get(CardViewUpdateService),
nodesApiService: NodesApiService = TestBed.get(NodesApiService);
spyOn(nodesApiService, 'updateNode');
updateService.update(property, 'updated-value');
expect(nodesApiService.updateNode).toHaveBeenCalledWith('node-id', {
'property-key': 'updated-value'
});
});
it('should update the node on successful save', async(() => {
const property = <CardViewBaseItemModel> { key: 'property-key', value: 'original-value' },
updateService: CardViewUpdateService = fixture.debugElement.injector.get(CardViewUpdateService),
nodesApiService: NodesApiService = TestBed.get(NodesApiService),
expectedNode = Object.assign({}, node, { name: 'some-modified-value' });
spyOn(nodesApiService, 'updateNode').and.callFake(() => {
return Observable.of(expectedNode);
});
updateService.update(property, 'updated-value');
fixture.whenStable().then(() => {
expect(component.node).toBe(expectedNode);
});
}));
it('should throw error on unsuccessful save', () => {
const property = <CardViewBaseItemModel> { key: 'property-key', value: 'original-value' },
updateService: CardViewUpdateService = fixture.debugElement.injector.get(CardViewUpdateService),
nodesApiService: NodesApiService = TestBed.get(NodesApiService),
logService: LogService = TestBed.get(LogService);
spyOn(nodesApiService, 'updateNode').and.callFake(() => {
return ErrorObservable.create(new Error('My bad'));
});
updateService.update(property, 'updated-value');
expect(logService.error).toHaveBeenCalledWith(new Error('My bad'));
});
});
describe('Properties loading', () => {
let expectedNode,
contentMetadataService: ContentMetadataService;
beforeEach(() => {
expectedNode = Object.assign({}, node, { name: 'some-modified-value' });
contentMetadataService = TestBed.get(ContentMetadataService);
fixture.detectChanges();
});
it('should load the basic properties on node change', () => {
spyOn(contentMetadataService, 'getBasicProperties');
component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) });
expect(contentMetadataService.getBasicProperties).toHaveBeenCalledWith(expectedNode);
});
it('should pass through the loaded basic properties to the card view', async(() => {
const expectedProperties = [];
component.expanded = false;
fixture.detectChanges();
spyOn(contentMetadataService, 'getBasicProperties').and.callFake(() => {
return Observable.of(expectedProperties);
});
component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) });
component.basicProperties$.subscribe(() => {
fixture.detectChanges();
const basicPropertiesComponent = fixture.debugElement.query(By.directive(CardViewComponent)).componentInstance;
expect(basicPropertiesComponent.properties).toBe(expectedProperties);
});
}));
it('should pass through the displayEmpty to the card view of basic properties', async(() => {
component.displayEmpty = false;
fixture.detectChanges();
spyOn(contentMetadataService, 'getBasicProperties').and.returnValue(Observable.of([]));
component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) });
component.basicProperties$.subscribe(() => {
fixture.detectChanges();
const basicPropertiesComponent = fixture.debugElement.query(By.directive(CardViewComponent)).componentInstance;
expect(basicPropertiesComponent.displayEmpty).toBe(false);
});
}));
it('should load the aspect properties on node change', () => {
spyOn(contentMetadataService, 'getAspectProperties');
component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) });
expect(contentMetadataService.getAspectProperties).toHaveBeenCalledWith(expectedNode, 'custom-preset');
});
it('should pass through the loaded aspect properties to the card view', async(() => {
const expectedProperties = [];
component.expanded = true;
fixture.detectChanges();
spyOn(contentMetadataService, 'getAspectProperties').and.callFake(() => {
return Observable.of([{ properties: expectedProperties }]);
});
component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) });
component.basicProperties$.subscribe(() => {
fixture.detectChanges();
const firstAspectPropertiesComponent = fixture.debugElement.query(By.css('.adf-metadata-properties-aspect adf-card-view')).componentInstance;
expect(firstAspectPropertiesComponent.properties).toBe(expectedProperties);
});
}));
it('should pass through the displayEmpty to the card view of aspect properties', async(() => {
component.expanded = true;
component.displayEmpty = false;
fixture.detectChanges();
spyOn(contentMetadataService, 'getAspectProperties').and.returnValue(Observable.of([{ properties: [] }]));
component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) });
component.basicProperties$.subscribe(() => {
fixture.detectChanges();
const basicPropertiesComponent = fixture.debugElement.query(By.css('.adf-metadata-properties-aspect adf-card-view')).componentInstance;
expect(basicPropertiesComponent.displayEmpty).toBe(false);
});
}));
});
});

View File

@ -15,21 +15,20 @@
* limitations under the License.
*/
import { ChangeDetectionStrategy, Component, Input, OnChanges, OnInit, ViewEncapsulation } from '@angular/core';
import { Component, Input, OnChanges, OnInit, SimpleChanges, SimpleChange, ViewEncapsulation } from '@angular/core';
import { MinimalNodeEntryEntity } from 'alfresco-js-api';
import { Observable } from 'rxjs/Observable';
import { CardViewItem, CardViewUpdateService, FileSizePipe, NodesApiService } from '@alfresco/adf-core';
import { ContentMetadataService } from './content-metadata.service';
import { CardViewItem, CardViewUpdateService, NodesApiService, LogService } from '@alfresco/adf-core';
import { ContentMetadataService } from '../../services/content-metadata.service';
import { CardViewAspect } from '../../interfaces/content-metadata.interfaces';
@Component({
selector: 'adf-content-metadata',
templateUrl: './content-metadata.component.html',
styleUrls: ['./content-metadata.component.scss'],
host: { 'class': 'adf-content-metadata' },
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
providers: [ CardViewUpdateService ],
viewProviders: [ ContentMetadataService, FileSizePipe ]
providers: [ CardViewUpdateService ]
})
export class ContentMetadataComponent implements OnChanges, OnInit {
@ -40,43 +39,41 @@ export class ContentMetadataComponent implements OnChanges, OnInit {
editable: boolean = false;
@Input()
maxPropertiesToShow: number = Infinity;
displayEmpty: boolean = false;
properties: CardViewItem[] = [];
@Input()
expanded: boolean = false;
@Input()
preset: string;
basicProperties$: Observable<CardViewItem[]>;
aspects$: Observable<CardViewAspect[]>;
constructor(private contentMetadataService: ContentMetadataService,
private cardViewUpdateService: CardViewUpdateService,
private nodesApi: NodesApiService) {}
private nodesApi: NodesApiService,
private logService: LogService) {}
ngOnInit(): void {
ngOnInit() {
this.cardViewUpdateService.itemUpdated$
.switchMap(this.saveNode.bind(this))
.subscribe(
node => this.node = node,
error => this.handleError(error)
error => this.logService.error(error)
);
}
ngOnChanges(): void {
this.recalculateProperties();
ngOnChanges(changes: SimpleChanges) {
const nodeChange: SimpleChange = changes['node'];
if (nodeChange) {
const node = nodeChange.currentValue;
this.basicProperties$ = this.contentMetadataService.getBasicProperties(node);
this.aspects$ = this.contentMetadataService.getAspectProperties(node, this.preset);
}
}
private saveNode({ changed: nodeBody }): Observable<MinimalNodeEntryEntity> {
return this.nodesApi.updateNode(this.node.id, nodeBody);
}
private handleError(error): void {
/*tslint:disable-next-line*/
console.log(error);
}
private recalculateProperties(): void {
let basicProperties = this.contentMetadataService.getBasicProperties(this.node);
if (this.maxPropertiesToShow) {
basicProperties = basicProperties.slice(0, this.maxPropertiesToShow);
}
this.properties = [...basicProperties];
}
}

View File

@ -1,25 +0,0 @@
<mat-card *ngIf="node">
<mat-card-content>
<adf-content-metadata [node]="node" [editable]="editable" [maxPropertiesToShow]="maxPropertiesToShow"></adf-content-metadata>
</mat-card-content>
<mat-card-footer class="adf-viewer-default-sidebar-card-footer" fxLayout="row" fxLayoutAlign="space-between stretch">
<div>
<button mat-icon-button>
<mat-icon>star_border</mat-icon>
</button>
<button mat-icon-button (click)="toggleEdit()">
<mat-icon>mode_edit</mat-icon>
</button>
</div>
<button mat-button (click)="toggleExpanded()">
<ng-container *ngIf="!expanded">
<span>{{ 'ADF_VIEWER.SIDEBAR.METADATA.MORE_INFORMATION' | translate }}</span>
<mat-icon>keyboard_arrow_down</mat-icon>
</ng-container>
<ng-container *ngIf="expanded">
<span>{{ '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

@ -1,3 +0,0 @@
<div class="adf-metadata-properties">
<adf-card-view [properties]="properties" [editable]="editable"></adf-card-view>
</div>

View File

@ -1,29 +0,0 @@
@mixin adf-content-metadata-theme($theme) {
$primary: map-get($theme, primary);
$background: map-get($theme, background);
$foreground: map-get($theme, foreground);
.adf {
&-metadata-properties {
.adf-property {
display: block;
margin-bottom: 20px;
.adf-property-label {
display: block;
padding: 0;
font-size: 12px;
color: mat-color($foreground, text, 0.4);
}
.adf-property-value {
display: block;
padding: 0;
font-size: 14px;
color: mat-color($foreground, text, 0.87);
}
}
}
}
}

View File

@ -0,0 +1,7 @@
@import './components/content-metadata/content-metadata.component';
@import './components/content-metadata-card/content-metadata-card.component';
@mixin adf-content-metadata-module-theme($theme) {
@include adf-content-metadata-theme($theme);
@include adf-content-metadata-card-theme($theme);
}

View File

@ -20,10 +20,14 @@ import { FlexLayoutModule } from '@angular/flex-layout';
import { NgModule } from '@angular/core';
import { TranslateModule } from '@ngx-translate/core';
import { MaterialModule } from '../material.module';
import { CardViewModule } from '@alfresco/adf-core';
import { ContentMetadataComponent } from './content-metadata.component';
import { ContentMetadataCardComponent } from './content-metadata-card.component';
import { CardViewModule , FileSizePipe } from '@alfresco/adf-core';
import { ContentMetadataComponent } from './components/content-metadata/content-metadata.component';
import { ContentMetadataCardComponent } from './components/content-metadata-card/content-metadata-card.component';
import { PropertyDescriptorsService } from './services/property-descriptors.service';
import { PropertyDescriptorLoaderService } from './services/properties-loader.service';
import { AspectWhiteListService } from './services/aspect-whitelist.service';
import { BasicPropertiesService } from './services/basic-properties.service';
import { ContentMetadataService } from './services/content-metadata.service';
@NgModule({
imports: [
@ -40,6 +44,14 @@ import { ContentMetadataCardComponent } from './content-metadata-card.component'
declarations: [
ContentMetadataComponent,
ContentMetadataCardComponent
],
providers: [
ContentMetadataService,
PropertyDescriptorsService,
PropertyDescriptorLoaderService,
AspectWhiteListService,
BasicPropertiesService,
FileSizePipe
]
})
export class ContentMetadataModule {}

View File

@ -0,0 +1,26 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export interface AspectProperty {
name: string;
title: string;
description?: string;
dataType: string;
defaultValue?: any;
mandatory: boolean;
multiValued: boolean;
}

View File

@ -0,0 +1,25 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* 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 { AspectProperty } from "./aspect-property.interface";
export interface Aspect {
name: string;
title: string;
description: string;
properties: AspectProperty[]
}

View File

@ -0,0 +1,23 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* 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 { CardViewItem} from '@alfresco/adf-core';
export interface CardViewAspect {
name: string;
properties: CardViewItem[]
}

View File

@ -0,0 +1,20 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './aspect-property.interface';
export * from './aspect.interface';
export * from './card-view-aspect.interface';

View File

@ -15,5 +15,4 @@
* limitations under the License.
*/
export * from './content-metadata.component';
export * from './content-metadata.service';
export * from './components/content-metadata-card/content-metadata-card.component';

View File

@ -0,0 +1,75 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Injectable } from '@angular/core';
import { AppConfigService, LogService } from '@alfresco/adf-core';
@Injectable()
export class AspectWhiteListService {
static readonly DEFAULT_PRESET = '*';
static readonly DEFAULT_PRESET_NAME = 'default';
preset: object | string = AspectWhiteListService.DEFAULT_PRESET;
constructor(private appConfigService: AppConfigService,
private logService: LogService) {}
public choosePreset(presetName: string) {
try {
const preset = this.appConfigService.config['content-metadata'].presets[presetName];
if (preset) {
this.preset = preset;
} else if (presetName !== AspectWhiteListService.DEFAULT_PRESET_NAME) {
this.logService.error(`No content-metadata preset for: ${presetName}`);
}
} catch (e) {
this.preset = AspectWhiteListService.DEFAULT_PRESET;
}
}
public isAspectAllowed(aspectName) {
if (this.isEveryAspectAllowed) {
return true;
}
const aspectNames = Object.keys(this.preset);
return aspectNames.indexOf(aspectName) !== -1;
}
public isPropertyAllowed(aspectName, propertyName) {
if (this.isEveryAspectAllowed || this.isEveryPropertyAllowedFor(aspectName)) {
return true;
}
if (this.preset[aspectName]) {
return this.preset[aspectName].indexOf(propertyName) !== -1;
}
return false;
}
private get isEveryAspectAllowed(): boolean {
return typeof this.preset === 'string' && this.preset === AspectWhiteListService.DEFAULT_PRESET;
}
private isEveryPropertyAllowedFor(aspectName): boolean {
const whitedListedProperties = this.preset[aspectName];
return typeof whitedListedProperties === 'string' && whitedListedProperties === AspectWhiteListService.DEFAULT_PRESET;
}
}

View File

@ -20,7 +20,7 @@ import { MinimalNodeEntryEntity } from 'alfresco-js-api';
import { CardViewDateItemModel, CardViewTextItemModel, FileSizePipe } from '@alfresco/adf-core';
@Injectable()
export class ContentMetadataService {
export class BasicPropertiesService {
constructor(private fileSizePipe: FileSizePipe) {}

View File

@ -0,0 +1,325 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* 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 { async, TestBed } from '@angular/core/testing';
import { ContentMetadataService } from './content-metadata.service';
import { PropertyDescriptorsService } from './property-descriptors.service';
import { BasicPropertiesService } from './basic-properties.service';
import { AspectWhiteListService } from './aspect-whitelist.service';
import { PropertyDescriptorLoaderService } from './properties-loader.service';
import { AlfrescoApiService } from '@alfresco/adf-core';
import { Observable } from 'rxjs/Observable';
import { Aspect, AspectProperty } from '../interfaces/content-metadata.interfaces';
import { MinimalNodeEntryEntity } from 'alfresco-js-api';
import {
CardViewTextItemModel,
CardViewDateItemModel,
CardViewIntItemModel,
CardViewFloatItemModel,
LogService,
CardViewBoolItemModel,
CardViewDatetimeItemModel
} from '@alfresco/adf-core';
describe('ContentMetadataService', () => {
let service: ContentMetadataService,
descriptorsService: PropertyDescriptorsService,
aspects: Aspect[],
aspect: Aspect,
aspectProperty: AspectProperty,
node: MinimalNodeEntryEntity;
const dummyPreset = 'default';
beforeEach(async(() => {
TestBed.configureTestingModule({
providers: [
ContentMetadataService,
BasicPropertiesService,
AspectWhiteListService,
PropertyDescriptorLoaderService,
AlfrescoApiService,
{ provide: LogService, useValue: { error: () => {} }},
PropertyDescriptorsService
]
}).compileComponents();
}));
beforeEach(() => {
service = TestBed.get(ContentMetadataService);
descriptorsService = TestBed.get(PropertyDescriptorsService);
node = <MinimalNodeEntryEntity> { properties: {} };
aspectProperty = {
name: 'FAS:PLAGUE',
title: 'The Faro Plague',
dataType: '',
defaultValue: '',
mandatory: false,
multiValued: false
};
aspect = {
name: 'FAS:FAS',
title: 'Faro Automated Solutions',
description: 'Faro Automated Solutions is an old Earth corporation that manufactured robots in the mid-21st century.',
properties: [aspectProperty]
};
aspects = [];
spyOn(descriptorsService, 'getAspects').and.returnValue(Observable.of(aspects));
});
afterEach(() => {
TestBed.resetTestingModule();
});
describe('General transformation', () => {
it('should translate more properties in an aspect properly', () => {
aspect.properties = [{
name: 'FAS:PLAGUE',
title: 'title',
dataType: 'd:text',
defaultValue: 'defaultValue',
mandatory: false,
multiValued: false
},
{
name: 'FAS:ALOY',
title: 'title',
dataType: 'd:text',
defaultValue: 'defaultValue',
mandatory: false,
multiValued: false
}];
aspects.push(aspect);
node.properties = { 'FAS:PLAGUE': 'The Chariot Line' };
service.getAspectProperties(node, dummyPreset).subscribe((cardViewAspect) => {
expect(cardViewAspect[0].properties.length).toBe(2);
expect(cardViewAspect[0].properties[0] instanceof CardViewTextItemModel).toBeTruthy('First property should be instance of CardViewTextItemModel');
expect(cardViewAspect[0].properties[1] instanceof CardViewTextItemModel).toBeTruthy('Second property should be instance of CardViewTextItemModel');
});
});
it('should translate every property in every aspect properly', () => {
aspects.push(
Object.assign({}, aspect, {
properties: [{
name: 'FAS:PLAGUE',
title: 'title',
dataType: 'd:text',
defaultValue: 'defaultvalue',
mandatory: false,
multiValued: false
}]
}),
Object.assign({}, aspect, {
properties: [{
name: 'FAS:ALOY',
title: 'title',
dataType: 'd:text',
defaultValue: 'defaultvalue',
mandatory: false,
multiValued: false
}]
})
);
node.properties = { 'FAS:PLAGUE': 'The Chariot Line' };
service.getAspectProperties(node, dummyPreset).subscribe((cardViewAspect) => {
expect(cardViewAspect.length).toBe(2);
expect(cardViewAspect[0].properties[0] instanceof CardViewTextItemModel).toBeTruthy('First aspect\'s property should be instance of CardViewTextItemModel');
expect(cardViewAspect[1].properties[0] instanceof CardViewTextItemModel).toBeTruthy('Second aspect\'s property should be instance of CardViewTextItemModel');
});
});
it('should log an error if unrecognised type is found', () => {
const logService = TestBed.get(LogService);
spyOn(logService, 'error').and.stub();
aspectProperty.name = 'FAS:PLAGUE';
aspectProperty.title = 'The Faro Plague';
aspectProperty.dataType = 'daemonic:scorcher';
aspectProperty.defaultValue = 'Daemonic beast';
aspects.push(aspect);
service.getAspectProperties(node, dummyPreset).subscribe((cardViewAspect) => {
expect(logService.error).toHaveBeenCalledWith('Unknown type for mapping: daemonic:scorcher');
});
});
it('should fall back to singleline property type if unrecognised type is found', () => {
aspectProperty.name = 'FAS:PLAGUE';
aspectProperty.title = 'The Faro Plague';
aspectProperty.dataType = 'daemonic:scorcher';
aspectProperty.defaultValue = 'Daemonic beast';
aspects.push(aspect);
service.getAspectProperties(node, dummyPreset).subscribe((cardViewAspect) => {
const property: CardViewTextItemModel = <CardViewTextItemModel> cardViewAspect[0].properties[0];
expect(property instanceof CardViewTextItemModel).toBeTruthy('Property should be instance of CardViewTextItemModel');
});
});
});
describe('Different types\'s attributes', () => {
ContentMetadataService.RECOGNISED_ECM_TYPES.forEach((dataType) => {
it(`should translate properly the basic attributes of a property for ${dataType}`, () => {
aspectProperty.name = 'prefix:name';
aspectProperty.title = 'title';
aspectProperty.defaultValue = 'default value';
aspectProperty.dataType = dataType;
aspects.push(aspect);
node.properties = { 'prefix:name': null };
service.getAspectProperties(node, dummyPreset).subscribe((cardViewAspect) => {
const property = cardViewAspect[0].properties[0];
expect(property.label).toBe(aspectProperty.title);
expect(property.key).toBe('properties.prefix:name');
expect(property.default).toBe(aspectProperty.defaultValue);
expect(property.editable).toBeTruthy('Property should be editable');
});
});
});
it('should translate properly the multiline and value attributes for d:text', () => {
aspectProperty.dataType = 'd:text';
aspects.push(aspect);
node.properties = { 'FAS:PLAGUE': 'The Chariot Line' };
service.getAspectProperties(node, dummyPreset).subscribe((cardViewAspect) => {
const property: CardViewTextItemModel = <CardViewTextItemModel> cardViewAspect[0].properties[0];
expect(property instanceof CardViewTextItemModel).toBeTruthy('Property should be instance of CardViewTextItemModel');
expect(property.value).toBe('The Chariot Line');
expect(property.multiline).toBeFalsy('Property should be singleline');
});
});
it('should translate properly the multiline and value attributes for d:mltext', () => {
aspectProperty.dataType = 'd:mltext';
aspects.push(aspect);
node.properties = { 'FAS:PLAGUE': 'The Chariot Line' };
service.getAspectProperties(node, dummyPreset).subscribe((cardViewAspect) => {
const property: CardViewTextItemModel = <CardViewTextItemModel> cardViewAspect[0].properties[0];
expect(property instanceof CardViewTextItemModel).toBeTruthy('Property should be instance of CardViewTextItemModel');
expect(property.value).toBe('The Chariot Line');
expect(property.multiline).toBeTruthy('Property should be multiline');
});
});
it('should translate properly the value attribute for d:date', () => {
const expectedValue = new Date().toISOString();
aspectProperty.dataType = 'd:date';
aspects.push(aspect);
node.properties = { 'FAS:PLAGUE': expectedValue };
service.getAspectProperties(node, dummyPreset).subscribe((cardViewAspect) => {
const property: CardViewDateItemModel = <CardViewDateItemModel> cardViewAspect[0].properties[0];
expect(property instanceof CardViewDateItemModel).toBeTruthy('Property should be instance of CardViewDateItemModel');
expect(property.value).toBe(expectedValue);
});
});
it('should translate properly the value attribute for d:date', () => {
const expectedValue = new Date().toISOString();
aspectProperty.dataType = 'd:datetime';
aspects.push(aspect);
node.properties = { 'FAS:PLAGUE': expectedValue };
service.getAspectProperties(node, dummyPreset).subscribe((cardViewAspect) => {
const property: CardViewDatetimeItemModel = <CardViewDatetimeItemModel> cardViewAspect[0].properties[0];
expect(property instanceof CardViewDatetimeItemModel).toBeTruthy('Property should be instance of CardViewDatetimeItemModel');
expect(property.value).toBe(expectedValue);
});
});
it('should translate properly the value attribute for d:int', () => {
aspectProperty.dataType = 'd:int';
aspects.push(aspect);
node.properties = { 'FAS:PLAGUE': '1024' };
service.getAspectProperties(node, dummyPreset).subscribe((cardViewAspect) => {
const property: CardViewIntItemModel = <CardViewIntItemModel> cardViewAspect[0].properties[0];
expect(property instanceof CardViewIntItemModel).toBeTruthy('Property should be instance of CardViewIntItemModel');
expect(property.value).toBe(1024);
});
});
it('should translate properly the value attribute for d:long', () => {
aspectProperty.dataType = 'd:long';
aspects.push(aspect);
node.properties = { 'FAS:PLAGUE': '1024' };
service.getAspectProperties(node, dummyPreset).subscribe((cardViewAspect) => {
const property: CardViewIntItemModel = <CardViewIntItemModel> cardViewAspect[0].properties[0];
expect(property instanceof CardViewIntItemModel).toBeTruthy('Property should be instance of CardViewIntItemModel');
expect(property.value).toBe(1024);
});
});
it('should translate properly the value attribute for d:float', () => {
aspectProperty.dataType = 'd:float';
aspects.push(aspect);
node.properties = { 'FAS:PLAGUE': '1024.24' };
service.getAspectProperties(node, dummyPreset).subscribe((cardViewAspect) => {
const property: CardViewFloatItemModel = <CardViewFloatItemModel> cardViewAspect[0].properties[0];
expect(property instanceof CardViewFloatItemModel).toBeTruthy('Property should be instance of CardViewFloatItemModel');
expect(property.value).toBe(1024.24);
});
});
it('should translate properly the value attribute for d:double', () => {
aspectProperty.dataType = 'd:double';
aspects.push(aspect);
node.properties = { 'FAS:PLAGUE': '1024.24' };
service.getAspectProperties(node, dummyPreset).subscribe((cardViewAspect) => {
const property: CardViewFloatItemModel = <CardViewFloatItemModel> cardViewAspect[0].properties[0];
expect(property instanceof CardViewFloatItemModel).toBeTruthy('Property should be instance of CardViewFloatItemModel');
expect(property.value).toBe(1024.24);
});
});
it('should translate properly the value attribute for d:boolean', () => {
aspectProperty.dataType = 'd:boolean';
aspects.push(aspect);
node.properties = { 'FAS:PLAGUE': true };
service.getAspectProperties(node, dummyPreset).subscribe((cardViewAspect) => {
const property: CardViewBoolItemModel = <CardViewBoolItemModel> cardViewAspect[0].properties[0];
expect(property instanceof CardViewBoolItemModel).toBeTruthy('Property should be instance of CardViewBoolItemModel');
expect(property.value).toBe(true);
});
});
});
});

View File

@ -0,0 +1,139 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Injectable } from '@angular/core';
import { MinimalNodeEntryEntity } from 'alfresco-js-api';
import { PropertyDescriptorsService } from './property-descriptors.service';
import { BasicPropertiesService } from './basic-properties.service';
import {
CardViewItemProperties,
CardViewItem,
CardViewTextItemModel,
CardViewBoolItemModel,
CardViewDateItemModel,
CardViewDatetimeItemModel,
CardViewIntItemModel,
CardViewFloatItemModel,
LogService
} from '@alfresco/adf-core';
import { Observable } from 'rxjs/Observable';
import { AspectProperty, CardViewAspect, Aspect } from '../interfaces/content-metadata.interfaces';
const D_TEXT = 'd:text';
const D_MLTEXT = 'd:mltext';
const D_DATE = 'd:date';
const D_DATETIME = 'd:datetime';
const D_INT = 'd:int';
const D_LONG = 'd:long';
const D_FLOAT = 'd:float';
const D_DOUBLE = 'd:double';
const D_BOOLEAN = 'd:boolean';
@Injectable()
export class ContentMetadataService {
static readonly RECOGNISED_ECM_TYPES = [ D_TEXT, D_MLTEXT, D_DATE, D_DATETIME, D_INT, D_LONG , D_FLOAT, D_DOUBLE, D_BOOLEAN ];
constructor(private basicPropertiesService: BasicPropertiesService,
private propertyDescriptorsService: PropertyDescriptorsService,
private logService: LogService) {}
getBasicProperties(node: MinimalNodeEntryEntity): Observable<CardViewItem[]> {
return Observable.of(this.basicPropertiesService.getBasicProperties(node));
}
getAspectProperties(node: MinimalNodeEntryEntity, preset: string): Observable<CardViewAspect[]> {
return this.propertyDescriptorsService.getAspects(node, preset)
.map(aspects => this.translateAspects(aspects, node.properties));
}
private translateAspects(aspects: Aspect[], nodeProperties): CardViewAspect[] {
return aspects.map(aspect => {
const translatedAspect: any = Object.assign({}, aspect);
translatedAspect.properties = this.translateProperties(aspect.properties, nodeProperties);
return translatedAspect;
});
}
private translateProperties(aspectProperties: AspectProperty[], nodeProperties: any): CardViewItem[] {
return aspectProperties.map(aspectProperty => {
return this.translateProperty(aspectProperty, nodeProperties[aspectProperty.name]);
});
}
private translateProperty(aspectProperty: AspectProperty, nodeProperty: any): CardViewItem {
this.checkECMTypeValidity(aspectProperty.dataType);
let propertyDefinition: CardViewItemProperties = {
label: aspectProperty.title,
value: nodeProperty,
key: this.getAspectPropertyKey(aspectProperty.name),
default: aspectProperty.defaultValue,
editable: true
};
let property;
switch (aspectProperty.dataType) {
case D_MLTEXT:
property = new CardViewTextItemModel(Object.assign(propertyDefinition, {
multiline: true
}));
break;
case D_INT:
case D_LONG:
property = new CardViewIntItemModel(propertyDefinition);
break;
case D_FLOAT:
case D_DOUBLE:
property = new CardViewFloatItemModel(propertyDefinition);
break;
case D_DATE:
property = new CardViewDateItemModel(propertyDefinition);
break;
case D_DATETIME:
property = new CardViewDatetimeItemModel(propertyDefinition);
break;
case D_BOOLEAN:
property = new CardViewBoolItemModel(propertyDefinition);
break;
case D_TEXT:
default:
property = new CardViewTextItemModel(Object.assign(propertyDefinition, {
multiline: false
}));
}
return property;
}
private checkECMTypeValidity(ecmPropertyType) {
if (ContentMetadataService.RECOGNISED_ECM_TYPES.indexOf(ecmPropertyType) === -1) {
this.logService.error(`Unknown type for mapping: ${ecmPropertyType}`);
}
}
private getAspectPropertyKey(propertyName) {
return `properties.${propertyName}`;
}
}

View File

@ -0,0 +1,95 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* 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 { async, TestBed } from '@angular/core/testing';
import { PropertyDescriptorLoaderService } from './properties-loader.service';
import { AlfrescoApiService } from '@alfresco/adf-core';
import { Observable } from 'rxjs/Observable';
import { ClassesApi } from 'alfresco-js-api';
describe('PropertyDescriptorLoaderService', () => {
let aspectProperties: PropertyDescriptorLoaderService,
classesApi: ClassesApi;
beforeEach(async(() => {
TestBed.configureTestingModule({
providers: [
PropertyDescriptorLoaderService,
AlfrescoApiService
]
}).compileComponents();
}));
beforeEach(() => {
aspectProperties = TestBed.get(PropertyDescriptorLoaderService);
const alfrescoApiService = TestBed.get(AlfrescoApiService);
classesApi = alfrescoApiService.classesApi;
});
afterEach(() => {
TestBed.resetTestingModule();
});
it('should load the aspects passed by paramter', () => {
spyOn(classesApi, 'getClass');
aspectProperties.load(['exif:exif', 'cm:content', 'custom:custom'])
.subscribe(() => {});
expect(classesApi.getClass).toHaveBeenCalledTimes(3);
expect(classesApi.getClass).toHaveBeenCalledWith('exif_exif');
expect(classesApi.getClass).toHaveBeenCalledWith('cm_content');
expect(classesApi.getClass).toHaveBeenCalledWith('custom_custom');
});
it('should merge the forked values', (done) => {
const exifResponse = {
name: 'exif:exif',
id: 1,
properties: {
'exif:1': { id: 'exif:1:id', name: 'exif:1' },
'exif:2': { id: 'exif:2:id', name: 'exif:2' }
}
};
const contentResponse = {
name: 'cm:content',
id: 2,
properties: {
'cm:content': { id: 'cm:content:id', name: 'cm:content' }
}
};
const apiResponses = [ exifResponse, contentResponse ];
let counter = 0;
spyOn(classesApi, 'getClass').and.callFake(() => {
return Observable.of(apiResponses[counter++]);
});
aspectProperties.load(['exif:exif', 'cm:content'])
.subscribe({
next: (data) => {
expect(data[0]).toBe(exifResponse);
expect(data[1]).toBe(contentResponse);
},
complete: done
});
});
});

View File

@ -0,0 +1,36 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Injectable } from '@angular/core';
import { AlfrescoApiService } from '@alfresco/adf-core';
import { forkJoin } from 'rxjs/observable/forkJoin';
import { Observable } from 'rxjs/Observable';
import { defer } from 'rxjs/observable/defer';
@Injectable()
export class PropertyDescriptorLoaderService {
constructor(private alfrescoApiService: AlfrescoApiService) {}
load(aspects: string[]): Observable<any> {
const aspectFetchStreams = aspects
.map(aspectName => aspectName.replace(':', '_'))
.map(aspectName => defer( () => this.alfrescoApiService.classesApi.getClass(aspectName)) );
return forkJoin(aspectFetchStreams);
}
}

View File

@ -0,0 +1,209 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* 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 { async, TestBed } from '@angular/core/testing';
import { PropertyDescriptorsService } from './property-descriptors.service';
import { PropertyDescriptorLoaderService } from './properties-loader.service';
import { MinimalNodeEntryEntity } from 'alfresco-js-api';
import { AlfrescoApiService } from '@alfresco/adf-core';
import { AspectWhiteListService } from './aspect-whitelist.service';
import { AppConfigService, LogService } from '@alfresco/adf-core';
import { Observable } from 'rxjs/Observable';
import { AspectProperty } from '../interfaces/content-metadata.interfaces';
describe('PropertyDescriptorsService', () => {
let contentMetadataService: PropertyDescriptorsService,
aspectProperties: PropertyDescriptorLoaderService,
appConfigService: AppConfigService,
logService: LogService,
node: MinimalNodeEntryEntity,
testPresets: any;
beforeEach(async(() => {
TestBed.configureTestingModule({
providers: [
PropertyDescriptorsService,
PropertyDescriptorLoaderService,
AppConfigService,
AspectWhiteListService,
{ provide: LogService, useValue: { error: () => {} }},
AlfrescoApiService
]
}).compileComponents();
}));
beforeEach(() => {
contentMetadataService = TestBed.get(PropertyDescriptorsService);
aspectProperties = TestBed.get(PropertyDescriptorLoaderService);
appConfigService = TestBed.get(AppConfigService);
logService = TestBed.get(LogService);
});
afterEach(() => {
TestBed.resetTestingModule();
});
describe('getAspects', () => {
beforeEach(() => {
node = <MinimalNodeEntryEntity> { aspectNames: [ 'exif:exif', 'cm:content', 'custom:custom' ] };
testPresets = {};
appConfigService.config['content-metadata'] = {
presets: testPresets
};
});
it('should call the aspect properties loading for the default aspects related to the given node and defined in the app config', () => {
spyOn(aspectProperties, 'load').and.callFake(x => Observable.of({}));
testPresets.default = { 'exif:exif': [], 'custom:custom': [], 'banana:banana': [] };
contentMetadataService.getAspects(node);
expect(aspectProperties.load).toHaveBeenCalledWith(['exif:exif', 'custom:custom']);
});
it('should call the aspect properties loading for the defined aspects related to the given node and defined in the app config', () => {
spyOn(aspectProperties, 'load').and.callFake(x => Observable.of({}));
testPresets.pink = { 'cm:content': [], 'custom:custom': [] };
contentMetadataService.getAspects(node, 'pink');
expect(aspectProperties.load).toHaveBeenCalledWith(['cm:content', 'custom:custom']);
});
it('should call the aspect properties loading for all the node aspectNames if the "*" widecard is used for the preset', () => {
spyOn(aspectProperties, 'load').and.callFake(x => Observable.of({}));
testPresets.default = '*';
contentMetadataService.getAspects(node);
expect(aspectProperties.load).toHaveBeenCalledWith(['exif:exif', 'cm:content', 'custom:custom']);
});
it('should call the aspect properties loading for all the node aspectNames if there is no preset data defined in the app config', () => {
spyOn(aspectProperties, 'load').and.callFake(x => Observable.of({}));
spyOn(logService, 'error').and.stub();
appConfigService.config['content-metadata'] = undefined;
contentMetadataService.getAspects(node);
expect(logService.error).not.toHaveBeenCalled();
expect(aspectProperties.load).toHaveBeenCalledWith(['exif:exif', 'cm:content', 'custom:custom']);
});
it('should show meaningful error when invalid preset are given', () => {
spyOn(aspectProperties, 'load').and.callFake(x => Observable.of({}));
spyOn(logService, 'error').and.stub();
testPresets.pink = { 'cm:content': {}, 'custom:custom': {} };
contentMetadataService.getAspects(node, 'blue');
expect(logService.error).toHaveBeenCalledWith('No content-metadata preset for: blue');
});
it('should filter out properties which are not defined in the particular aspect', () => {
spyOn(aspectProperties, 'load').and.callFake(() => {
return Observable.of([
{
name: 'exif:exif',
properties: [
{ name: 'exif:1' },
{ name: 'exif:2' }
]
}
]);
});
testPresets.default = { 'exif:exif': ['exif:2'] };
contentMetadataService.getAspects(node).subscribe({
next: (aspects) => {
expect(aspects[0].name).toBe('exif:exif');
expect(aspects[0].properties).toContain(<AspectProperty> { name: 'exif:2' });
expect(aspects[0].properties).not.toContain(<AspectProperty> { name: 'exif:1' });
}
});
});
it('should accept "*" wildcard for aspect properties', () => {
spyOn(aspectProperties, 'load').and.callFake(() => {
return Observable.of([
{
name: 'exif:exif',
properties: [
{ name: 'exif:1' },
{ name: 'exif:2' }
]
},
{
name: 'custom:custom',
properties: [
{ name: 'custom:1' },
{ name: 'custom:2' }
]
}
]);
});
testPresets.default = {
'exif:exif': '*',
'custom:custom': ['custom:1']
};
contentMetadataService.getAspects(node).subscribe({
next: (aspects) => {
expect(aspects.length).toBe(2);
expect(aspects[0].name).toBe('exif:exif');
expect(aspects[0].properties).toContain(<AspectProperty> { name: 'exif:1' });
expect(aspects[0].properties).toContain(<AspectProperty> { name: 'exif:2' });
expect(aspects[1].name).toBe('custom:custom');
expect(aspects[1].properties).toContain(<AspectProperty> { name: 'custom:1' });
expect(aspects[1].properties).not.toContain(<AspectProperty> { name: 'custom:2' });
}
});
});
it('should filter out aspects which are not present in app config preset', () => {
spyOn(aspectProperties, 'load').and.callFake(() => {
return Observable.of([
{
name: 'exif:exif',
properties: [
{ name: 'exif:1' }
]
}
]);
});
testPresets.default = {
'exif:exif': ['exif:1'],
'banana:banana': ['banana:1']
};
contentMetadataService.getAspects(node).subscribe({
next: (aspects) => {
expect(aspects.length).toBe(1);
expect(aspects[0].name).toBe('exif:exif');
expect(aspects[0].properties).toContain(<AspectProperty> { name: 'exif:1' });
}
});
});
});
});

View File

@ -0,0 +1,60 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Injectable } from '@angular/core';
import { MinimalNodeEntryEntity } from 'alfresco-js-api';
import { PropertyDescriptorLoaderService } from './properties-loader.service';
import { AspectWhiteListService } from './aspect-whitelist.service';
import { Observable } from 'rxjs/Observable';
import { Aspect, AspectProperty } from '../interfaces/content-metadata.interfaces';
@Injectable()
export class PropertyDescriptorsService {
constructor(private aspectWhiteListService: AspectWhiteListService,
private aspectPropertiesService: PropertyDescriptorLoaderService) {}
getAspects(node: MinimalNodeEntryEntity, presetName: string = 'default'): Observable<Aspect[]> {
this.aspectWhiteListService.choosePreset(presetName);
return this.loadAspectDescriptors(node.aspectNames)
.map(this.filterPropertiesByWhitelist.bind(this));
}
private loadAspectDescriptors(aspectsAssignedToNode: string[]): Observable<any> {
const aspectsToLoad = aspectsAssignedToNode
.filter(nodeAspectName => this.aspectWhiteListService.isAspectAllowed(nodeAspectName));
return this.aspectPropertiesService.load(aspectsToLoad);
}
private filterPropertiesByWhitelist(aspectDescriptors): Aspect[] {
return aspectDescriptors.map((aspectDescriptor) => {
return Object.assign({}, aspectDescriptor, {
properties: this.getFilteredPropertiesArray(aspectDescriptor)
});
});
}
private getFilteredPropertiesArray(aspectDescriptor): AspectProperty[] {
const aspectName = aspectDescriptor.name;
return Object.keys(aspectDescriptor.properties)
.map(propertyName => aspectDescriptor.properties[propertyName])
.filter(property => this.aspectWhiteListService.isPropertyAllowed(aspectName, property.name));
}
}

View File

@ -188,6 +188,7 @@
},
"METADATA": {
"BASIC": {
"HEADER": "Properties",
"NAME": "Name",
"TITLE": "Title",
"DESCRIPTION": "Description",

View File

@ -29,6 +29,7 @@ import {
MatProgressBarModule,
MatProgressSpinnerModule,
MatRippleModule,
MatExpansionModule,
MatSelectModule
} from '@angular/material';
@ -46,6 +47,7 @@ export function modules() {
MatRippleModule,
MatMenuModule,
MatOptionModule,
MatExpansionModule,
MatSelectModule
];
}

View File

@ -12,9 +12,8 @@
@import '../dialogs/folder.dialog';
@import '../content-metadata/content-metadata.component';
@import '../content-metadata/content-metadata-card.component';
@import '../content-node-selector/content-node-selector.component';
@import '../content-metadata/content-metadata.module';
@mixin adf-content-services-theme($theme) {
@include adf-breadcrumb-theme($theme);
@ -27,7 +26,6 @@
@include adf-search-control-theme($theme);
@include adf-search-autocomplete-theme($theme);
@include adf-dialog-theme($theme);
@include adf-content-metadata-theme($theme);
@include adf-content-metadata-card-theme($theme);
@include adf-content-node-selector-dialog-theme($theme) ;
@include adf-content-metadata-module-theme($theme);
}

View File

@ -1,30 +0,0 @@
<div class="adf-property-label">{{ property.label | translate }}</div>
<div class="adf-property-value">
<span *ngIf="!isEditable()">
<span [attr.data-automation-id]="'card-dateitem-' + property.key">
<span *ngIf="!property.isEmpty(); else elseEmptyValueBlock">{{ property.displayValue }}</span>
</span>
</span>
<span *ngIf="isEditable()" class="adf-dateitem-editable">
<input class="adf-invisible-date-input" [matDatepicker]="picker" [value]="valueDate"
(dateChange)="onDateChanged($event)">
<span
class="adf-datepicker-toggle"
[attr.data-automation-id]="'datepicker-label-toggle-' + property.key"
(click)="showDatePicker($event)">
<span *ngIf="!property.isEmpty(); else elseEmptyValueBlock">{{ property.displayValue }}</span>
</span>
<mat-datepicker-toggle
[attr.data-automation-id]="'datepickertoggle-' + property.key"
[for]="picker">
</mat-datepicker-toggle>
<mat-datepicker #picker
[attr.data-automation-id]="'datepicker-' + property.key"
[startAt]="valueDate"
[touchUi]="true">
</mat-datepicker>
</span>
<ng-template #elseEmptyValueBlock>
{{ property.default | translate }}
</ng-template>
</div>

View File

@ -1,36 +0,0 @@
@mixin adf-card-view-dateitem-theme($theme) {
.adf {
&-invisible-date-input {
height: 24px;
width: 0;
overflow: hidden;
opacity: 0;
border: none;
margin: 0;
padding: 0;
display: none;
}
&-dateitem-editable {
cursor: pointer;
button.mat-icon-button {
line-height: 20px;
height: 20px;
width: 20px;
}
mat-icon {
width: 16px;
height: 16px;
opacity: 0.5;
margin-left: 4px;
}
&:hover mat-icon {
opacity: 1;
}
}
}
}

View File

@ -1,51 +0,0 @@
<div class="adf-property-label">{{ property.label | translate }}</div>
<div class="adf-property-value">
<span *ngIf="!isEditable()">
<span *ngIf="!isClickable(); else elseBlock" [attr.data-automation-id]="'card-textitem-value-' + property.key">
<span *ngIf="!property.isEmpty(); else elseEmptyValueBlock">{{ property.displayValue }}</span>
</span>
<ng-template #elseBlock>
<span class="adf-textitem-clickable-value" (click)="clicked()" [attr.data-automation-id]="'card-textitem-value-' + property.key">
<span *ngIf="!property.isEmpty(); else elseEmptyValueBlock">{{ property.displayValue }}</span>
</span>
</ng-template>
</span>
<span *ngIf="isEditable()">
<div *ngIf="!inEdit" (click)="setEditMode(true)" class="adf-textitem-readonly" [attr.data-automation-id]="'card-textitem-edit-toggle-' + property.key" fxLayout="row" fxLayoutAlign="space-between center">
<span [attr.data-automation-id]="'card-textitem-value-' + property.key">
<span *ngIf="!property.isEmpty(); else elseEmptyValueBlock">{{ property.displayValue }}</span>
</span>
<mat-icon fxFlex="0 0 auto" [attr.data-automation-id]="'card-textitem-edit-icon-' + property.key" class="adf-textitem-icon">create</mat-icon>
</div>
<div *ngIf="inEdit" class="adf-textitem-editable">
<mat-form-field floatPlaceholder="never" class="adf-input-container">
<input *ngIf="!property.multiline" #editorInput
matInput
class="adf-input"
[placeholder]="property.default | translate"
[(ngModel)]="editedValue"
[attr.data-automation-id]="'card-textitem-editinput-' + property.key">
<textarea *ngIf="property.multiline" #editorInput
matInput
matTextareaAutosize
matAutosizeMaxRows="1"
matAutosizeMaxRows="5"
class="adf-textarea"
[placeholder]="property.default | translate"
[(ngModel)]="editedValue"
[attr.data-automation-id]="'card-textitem-edittextarea-' + property.key"></textarea>
</mat-form-field>
<mat-icon
class="adf-textitem-icon adf-update-icon"
(click)="update()"
[attr.data-automation-id]="'card-textitem-update-' + property.key">done</mat-icon>
<mat-icon
class="adf-textitem-icon adf-reset-icon"
(click)="reset()"
[attr.data-automation-id]="'card-textitem-reset-' + property.key">clear</mat-icon>
</div>
</span>
<ng-template #elseEmptyValueBlock>
{{ property.default | translate }}
</ng-template>
</div>

View File

@ -1,233 +0,0 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* 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 { HttpClientModule } from '@angular/common/http';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { MatDatepickerModule, MatIconModule, MatInputModule, MatNativeDateModule } from '@angular/material';
import { By } from '@angular/platform-browser';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { CardViewTextItemModel } from '../models/card-view-textitem.model';
import { AppConfigService } from '../app-config/app-config.service';
import { CardViewUpdateService } from '../services/card-view-update.service';
import { LogService } from '../services/log.service';
import { TranslateLoaderService } from '../services/translate-loader.service';
import { CardViewTextItemComponent } from './card-view-textitem.component';
describe('CardViewTextItemComponent', () => {
let fixture: ComponentFixture<CardViewTextItemComponent>;
let component: CardViewTextItemComponent;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
HttpClientModule,
FormsModule,
NoopAnimationsModule,
MatDatepickerModule,
MatIconModule,
MatInputModule,
MatNativeDateModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: TranslateLoaderService
}
})
],
declarations: [
CardViewTextItemComponent
],
providers: [
AppConfigService,
CardViewUpdateService,
LogService
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CardViewTextItemComponent);
component = fixture.componentInstance;
component.property = new CardViewTextItemModel ({
label: 'Text label',
value: 'Lorem ipsum',
key: 'textkey',
default: 'FAKE-DEFAULT-KEY',
editable: false
});
});
afterEach(() => {
fixture.destroy();
TestBed.resetTestingModule();
});
it('should render the label and value', () => {
fixture.detectChanges();
let labelValue = fixture.debugElement.query(By.css('.adf-property-label'));
expect(labelValue).not.toBeNull();
expect(labelValue.nativeElement.innerText).toBe('Text label');
let value = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-value-${component.property.key}"]`));
expect(value).not.toBeNull();
expect(value.nativeElement.innerText.trim()).toBe('Lorem ipsum');
});
it('should render the default as value if the value is empty and editable false', () => {
component.property = new CardViewTextItemModel ({
label: 'Text label',
value: '',
key: 'textkey',
default: 'FAKE-DEFAULT-KEY',
editable: false
});
fixture.detectChanges();
let value = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-value-${component.property.key}"]`));
expect(value).not.toBeNull();
expect(value.nativeElement.innerText.trim()).toBe('FAKE-DEFAULT-KEY');
});
it('should render the default as value if the value is empty and editable true', () => {
component.property = new CardViewTextItemModel ({
label: 'Text label',
value: '',
key: 'textkey',
default: 'FAKE-DEFAULT-KEY',
editable: true
});
fixture.detectChanges();
let value = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-value-${component.property.key}"]`));
expect(value).not.toBeNull();
expect(value.nativeElement.innerText.trim()).toBe('FAKE-DEFAULT-KEY');
});
it('should render the default as value if the value is empty and clickable false', () => {
component.property = new CardViewTextItemModel ({
label: 'Text label',
value: '',
key: 'textkey',
default: 'FAKE-DEFAULT-KEY',
clickable: false
});
fixture.detectChanges();
let value = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-value-${component.property.key}"]`));
expect(value).not.toBeNull();
expect(value.nativeElement.innerText.trim()).toBe('FAKE-DEFAULT-KEY');
});
it('should render the default as value if the value is empty and clickable true', () => {
component.property = new CardViewTextItemModel ({
label: 'Text label',
value: '',
key: 'textkey',
default: 'FAKE-DEFAULT-KEY',
clickable: true
});
fixture.detectChanges();
let value = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-value-${component.property.key}"]`));
expect(value).not.toBeNull();
expect(value.nativeElement.innerText.trim()).toBe('FAKE-DEFAULT-KEY');
});
it('should render value when editable:true', () => {
component.editable = true;
component.property.editable = true;
fixture.detectChanges();
let value = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-value-${component.property.key}"]`));
expect(value).not.toBeNull();
expect(value.nativeElement.innerText.trim()).toBe('Lorem ipsum');
});
it('should render the edit icon in case of editable:true', () => {
component.editable = true;
component.property.editable = true;
fixture.detectChanges();
let editIcon = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-edit-icon-${component.property.key}"]`));
expect(editIcon).not.toBeNull('Edit icon should be shown');
});
it('should NOT render the edit icon in case of editable:false', () => {
component.editable = false;
fixture.detectChanges();
let editIcon = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-edit-icon-${component.property.key}"]`));
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', () => {
component.editable = false;
component.property.editable = true;
fixture.detectChanges();
let editIcon = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-edit-icon-${component.property.key}"]`));
expect(editIcon).toBeNull('Edit icon should NOT be shown');
});
it('should trigger an update event on the CardViewUpdateService', (done) => {
component.editable = true;
component.property.editable = true;
const cardViewUpdateService = TestBed.get(CardViewUpdateService);
const expectedText = 'changed text';
fixture.detectChanges();
cardViewUpdateService.itemUpdated$.subscribe(
(updateNotification) => {
expect(updateNotification.target).toBe(component.property);
expect(updateNotification.changed).toEqual({ textkey: expectedText });
done();
}
);
let editIcon = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-edit-toggle-${component.property.key}"]`));
editIcon.triggerEventHandler('click', null);
fixture.detectChanges();
let editInput = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-editinput-${component.property.key}"]`));
editInput.nativeElement.value = expectedText;
editInput.nativeElement.dispatchEvent(new Event('input'));
fixture.detectChanges();
let updateInput = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-update-${component.property.key}"]`));
updateInput.triggerEventHandler('click', null);
});
it('should switch back to readonly mode after an update attempt', async(() => {
component.editable = true;
component.property.editable = true;
component.inEdit = true;
component.editedValue = 'updated-value';
fixture.detectChanges();
component.update();
fixture.whenStable().then(() => {
expect(component.property.value).toBe(component.editedValue);
expect(component.inEdit).toBeFalsy();
});
}));
});

View File

@ -1,8 +0,0 @@
<div class="adf-property-list">
<div *ngFor="let property of properties">
<div [attr.data-automation-id]="'header-'+property.key" class="adf-property">
<adf-card-view-item-dispatcher [property]="property" [editable]="editable"></adf-card-view-item-dispatcher>
</div>
</div>
</div>

View File

@ -1,34 +0,0 @@
@mixin adf-card-view-theme($theme) {
.adf {
&-property-list {
display: table;
width: 100%;
border-collapse: collapse;
border-spacing: 0;
}
&-property {
display: table-row;
}
&-property &-property-label {
display: table-cell;
min-width: 100px;
padding-right: 30px;
word-wrap: break-word;
color: rgb(186, 186, 186);;
vertical-align: top;
padding-bottom: 20px;
}
&-property &-property-value {
width: 100%;
display: table-cell;
color: rgb(101, 101, 101);
vertical-align: top;
padding-bottom: 20px;
}
}
}

View File

@ -0,0 +1,11 @@
@import './components/card-view-dateitem/card-view-dateitem.component';
@import './components/card-view-textitem/card-view-textitem.component';
@import './components/card-view/card-view.component';
@import '~@mat-datetimepicker/core/datetimepicker/datetimepicker-theme.scss';
@mixin adf-card-view-module-theme($theme) {
@include adf-card-view-dateitem-theme($theme);
@include adf-card-view-textitem-theme($theme);
@include adf-card-view-theme($theme);
@include mat-datetimepicker-theme($theme);
}

View File

@ -18,47 +18,69 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatButtonModule, MatDatepickerModule, MatIconModule, MatInputModule, MatNativeDateModule } from '@angular/material';
import {
MatButtonModule,
MatDatepickerModule,
MatIconModule,
MatInputModule,
MatCheckboxModule,
MatNativeDateModule
} from '@angular/material';
import { MatDatetimepickerModule, MatNativeDatetimeModule } from '@mat-datetimepicker/core';
import { FlexLayoutModule } from '@angular/flex-layout';
import { TranslateModule } from '@ngx-translate/core';
import { CardViewContentProxyDirective } from './card-view-content-proxy.directive';
import { CardViewDateItemComponent } from './card-view-dateitem.component';
import { CardViewItemDispatcherComponent } from './card-view-item-dispatcher.component';
import { CardViewMapItemComponent } from './card-view-mapitem.component';
import { CardViewTextItemComponent } from './card-view-textitem.component';
import { CardViewComponent } from './card-view.component';
import { CardViewContentProxyDirective } from './directives/card-view-content-proxy.directive';
import { CardViewComponent } from './components/card-view/card-view.component';
import { CardViewBoolItemComponent } from './components/card-view-boolitem/card-view-boolitem.component';
import { CardViewDateItemComponent } from './components/card-view-dateitem/card-view-dateitem.component';
import { CardViewItemDispatcherComponent } from './components/card-view-item-dispatcher/card-view-item-dispatcher.component';
import { CardViewMapItemComponent } from './components/card-view-mapitem/card-view-mapitem.component';
import { CardViewTextItemComponent } from './components/card-view-textitem/card-view-textitem.component';
import { CardItemTypeService } from './services/card-item-types.service';
import { CardViewUpdateService } from './services/card-view-update.service';
@NgModule({
imports: [
CommonModule,
FormsModule,
FlexLayoutModule,
TranslateModule,
MatDatepickerModule,
MatNativeDateModule,
MatCheckboxModule,
MatInputModule,
MatIconModule,
MatButtonModule,
FormsModule,
FlexLayoutModule,
TranslateModule
MatDatetimepickerModule,
MatNativeDatetimeModule
],
declarations: [
CardViewComponent,
CardViewItemDispatcherComponent,
CardViewContentProxyDirective,
CardViewTextItemComponent,
CardViewBoolItemComponent,
CardViewDateItemComponent,
CardViewMapItemComponent,
CardViewDateItemComponent
CardViewTextItemComponent,
CardViewItemDispatcherComponent,
CardViewContentProxyDirective
],
entryComponents: [
CardViewTextItemComponent,
CardViewBoolItemComponent,
CardViewDateItemComponent,
CardViewMapItemComponent,
CardViewDateItemComponent
CardViewTextItemComponent
],
exports: [
CardViewComponent,
CardViewTextItemComponent,
CardViewBoolItemComponent,
CardViewDateItemComponent,
CardViewMapItemComponent,
CardViewDateItemComponent
CardViewTextItemComponent
],
providers: [
CardItemTypeService,
CardViewUpdateService
]
})
export class CardViewModule {}

View File

@ -0,0 +1,10 @@
<ng-container *ngIf="!property.isEmpty() || isEditable()">
<div class="adf-property-label">{{ property.label | translate }}</div>
<div class="adf-property-value">
<mat-checkbox
[checked]="property.displayValue"
[disabled]="!isEditable()"
(change)="changed($event)">
</mat-checkbox>
</div>
</ng-container>

View File

@ -0,0 +1,243 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* 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 { HttpClientModule } from '@angular/common/http';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { MaterialModule } from '../../../material.module';
import { MatCheckboxChange, MatCheckbox } from '@angular/material';
import { By } from '@angular/platform-browser';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { AppConfigService } from '../../../app-config/app-config.service';
import { CardViewUpdateService } from '../../services/card-view-update.service';
import { LogService } from '../../../services/log.service';
import { TranslateLoaderService } from '../../../services/translate-loader.service';
import { CardViewBoolItemComponent } from './card-view-boolitem.component';
import { CardViewBoolItemModel } from '../../models/card-view-boolitem.model';
describe('CardViewBoolItemComponent', () => {
let fixture: ComponentFixture<CardViewBoolItemComponent>;
let component: CardViewBoolItemComponent;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
HttpClientModule,
FormsModule,
NoopAnimationsModule,
MaterialModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: TranslateLoaderService
}
})
],
declarations: [
CardViewBoolItemComponent
],
providers: [
AppConfigService,
CardViewUpdateService,
LogService
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CardViewBoolItemComponent);
component = fixture.componentInstance;
component.property = new CardViewBoolItemModel({
label: 'Boolean label',
value: true,
key: 'boolkey',
default: false,
editable: false
});
});
afterEach(() => {
fixture.destroy();
TestBed.resetTestingModule();
});
describe('Rendering', () => {
it('should render the label and value if the property is editable', () => {
component.editable = true;
component.property.editable = true;
fixture.detectChanges();
let label = fixture.debugElement.query(By.css('.adf-property-label'));
expect(label).not.toBeNull();
expect(label.nativeElement.innerText).toBe('Boolean label');
let value = fixture.debugElement.query(By.css('.adf-property-value'));
expect(value).not.toBeNull();
});
it('should NOT render the label and value if the property is NOT editable and doesn\'t have a proper boolean value set' , () => {
component.editable = true;
component.property.value = undefined;
component.property.editable = false;
fixture.detectChanges();
let label = fixture.debugElement.query(By.css('.adf-property-label'));
expect(label).toBeNull();
let value = fixture.debugElement.query(By.css('.adf-property-value'));
expect(value).toBeNull();
});
it('should render the label and value if the property is NOT editable but has a proper boolean value set' , () => {
component.editable = true;
component.property.value = false;
component.property.editable = false;
fixture.detectChanges();
let label = fixture.debugElement.query(By.css('.adf-property-label'));
expect(label).not.toBeNull();
let value = fixture.debugElement.query(By.css('.adf-property-value'));
expect(value).not.toBeNull();
});
it('should render ticked checkbox if property\'s value is true', () => {
component.property.value = true;
fixture.detectChanges();
let value = fixture.debugElement.query(By.css('.adf-property-value input[type="checkbox"]'));
expect(value).not.toBeNull();
expect(value.nativeElement.checked).toBe(true);
});
it('should render ticked checkbox if property\'s value is not set but default is true and editable', () => {
component.editable = true;
component.property.editable = true;
component.property.value = undefined;
component.property.default = true;
fixture.detectChanges();
let value = fixture.debugElement.query(By.css('.adf-property-value input[type="checkbox"]'));
expect(value).not.toBeNull();
expect(value.nativeElement.checked).toBe(true);
});
it('should render unticked checkbox if property\'s value is false', () => {
component.property.value = false;
fixture.detectChanges();
let value = fixture.debugElement.query(By.css('.adf-property-value input[type="checkbox"]'));
expect(value).not.toBeNull();
expect(value.nativeElement.checked).toBe(false);
});
it('should render unticked checkbox if property\'s value is not set but default is false and editable', () => {
component.editable = true;
component.property.editable = true;
component.property.value = undefined;
component.property.default = false;
fixture.detectChanges();
let value = fixture.debugElement.query(By.css('.adf-property-value input[type="checkbox"]'));
expect(value).not.toBeNull();
expect(value.nativeElement.checked).toBe(false);
});
it('should render enabled checkbox if property and component are both editable', () => {
component.editable = true;
component.property.editable = true;
component.property.value = true;
fixture.detectChanges();
let value = fixture.debugElement.query(By.css('.adf-property-value input[type="checkbox"]'));
expect(value).not.toBeNull();
expect(value.nativeElement.hasAttribute('disabled')).toBe(false);
});
it('should render disabled checkbox if property is not editable', () => {
component.editable = true;
component.property.editable = false;
component.property.value = true;
fixture.detectChanges();
let value = fixture.debugElement.query(By.css('.adf-property-value input[type="checkbox"]'));
expect(value).not.toBeNull();
expect(value.nativeElement.hasAttribute('disabled')).toBe(true);
});
it('should render disabled checkbox if component is not editable', () => {
component.editable = false;
component.property.editable = true;
component.property.value = true;
fixture.detectChanges();
let value = fixture.debugElement.query(By.css('.adf-property-value input[type="checkbox"]'));
expect(value).not.toBeNull();
expect(value.nativeElement.hasAttribute('disabled')).toBe(true);
});
});
describe('Update', () => {
beforeEach(() => {
component.editable = true;
component.property.editable = true;
component.property.value = undefined;
fixture.detectChanges();
});
it('should trigger the update event when changing the checkbox', () => {
const cardViewUpdateService = TestBed.get(CardViewUpdateService);
spyOn(cardViewUpdateService, 'update');
component.changed(<MatCheckboxChange> {checked: true});
expect(cardViewUpdateService.update).toHaveBeenCalledWith(component.property, true);
});
it('should update the propery\'s value after a changed', async(() => {
component.property.value = true;
component.changed(<MatCheckboxChange> {checked: false});
fixture.whenStable().then(() => {
expect(component.property.value).toBe(false);
});
}));
it('should trigger an update event on the CardViewUpdateService [integration]', (done) => {
const cardViewUpdateService = TestBed.get(CardViewUpdateService);
component.property.value = false;
fixture.detectChanges();
cardViewUpdateService.itemUpdated$.subscribe(
(updateNotification) => {
expect(updateNotification.target).toBe(component.property);
expect(updateNotification.changed).toEqual({ boolkey: true });
done();
}
);
const labelElement = fixture.debugElement.query(By.directive(MatCheckbox)).nativeElement.querySelector('label');
labelElement.click();
});
});
});

View File

@ -0,0 +1,47 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Component, Input } from '@angular/core';
import { MatCheckboxChange } from '@angular/material';
import { CardViewBoolItemModel } from '../../models/card-view-boolitem.model';
import { CardViewUpdateService } from '../../services/card-view-update.service';
@Component({
selector: 'adf-card-view-boolitem',
templateUrl: './card-view-boolitem.component.html',
styleUrls: ['./card-view-boolitem.component.scss']
})
export class CardViewBoolItemComponent {
@Input()
property: CardViewBoolItemModel;
@Input()
editable: boolean;
constructor(private cardViewUpdateService: CardViewUpdateService) {}
isEditable() {
return this.editable && this.property.editable;
}
changed(change: MatCheckboxChange) {
this.cardViewUpdateService.update(this.property, change.checked );
this.property.value = change.checked;
}
}

View File

@ -0,0 +1,37 @@
<div class="adf-property-label" *ngIf="showProperty() || isEditable()">{{ property.label | translate }}</div>
<div class="adf-property-value">
<span *ngIf="!isEditable()">
<span [attr.data-automation-id]="'card-dateitem-' + property.key">
<span *ngIf="showProperty()">{{ property.displayValue }}</span>
</span>
</span>
<div *ngIf="isEditable()" 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($event)">
<span *ngIf="showProperty(); else elseEmptyValueBlock">{{ property.displayValue }}</span>
</span>
<mat-datetimepicker-toggle
[attr.data-automation-id]="'datepickertoggle-' + property.key"
[for]="datetimePicker">
</mat-datetimepicker-toggle>
</div>
<input class="adf-invisible-date-input"
[matDatetimepicker]="datetimePicker"
[value]="valueDate"
(dateChange)="onDateChanged($event)">
<mat-datetimepicker #datetimePicker
[type]="property.type"
timeInterval="5"
[attr.data-automation-id]="'datepicker-' + property.key"
[startAt]="valueDate">
</mat-datetimepicker>
</div>
<ng-template #elseEmptyValueBlock>
{{ property.default | translate }}
</ng-template>
</div>

View File

@ -0,0 +1,41 @@
@mixin adf-card-view-dateitem-theme($theme) {
.adf {
&-invisible-date-input {
height: 2px;
width: 0;
overflow: hidden;
opacity: 0;
border: none;
margin: 0;
padding: 0;
float: right;
}
&-dateitem-editable {
cursor: pointer;
&-controls {
display: flex;
align-items: center;
justify-content: space-between;
button.mat-icon-button {
line-height: 20px;
height: 20px;
width: 20px;
}
mat-icon {
width: 16px;
height: 16px;
opacity: 0.5;
}
&:hover mat-icon {
opacity: 1;
}
}
}
}
}

View File

@ -18,13 +18,14 @@
import { HttpClientModule } from '@angular/common/http';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { MatDatepickerModule, MatInputModule, MatNativeDateModule } from '@angular/material';
import { MatDatetimepickerModule, MatNativeDatetimeModule } from '@mat-datetimepicker/core';
import { By } from '@angular/platform-browser';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import moment from 'moment-es6';
import { AppConfigService } from '../index';
import { CardViewDateItemModel } from '../models/card-view-dateitem.model';
import { CardViewUpdateService } from '../services/card-view-update.service';
import { TranslateLoaderService } from '../services/translate-loader.service';
import { AppConfigService } from '../../../index';
import { CardViewDateItemModel } from '../../models/card-view-dateitem.model';
import { CardViewUpdateService } from '../../services/card-view-update.service';
import { TranslateLoaderService } from '../../../services/translate-loader.service';
import { CardViewDateItemComponent } from './card-view-dateitem.component';
@ -40,6 +41,8 @@ describe('CardViewDateItemComponent', () => {
MatDatepickerModule,
MatInputModule,
MatNativeDateModule,
MatNativeDatetimeModule,
MatDatetimepickerModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
@ -87,7 +90,7 @@ describe('CardViewDateItemComponent', () => {
expect(value.nativeElement.innerText.trim()).toBe('Jul 10 2017');
});
it('should render the default as value if the value is empty and editable:false', () => {
it('should NOT render the default as value if the value is empty, editable:false and displayEmpty is false', () => {
component.property = new CardViewDateItemModel ({
label: 'Date label',
value: '',
@ -96,6 +99,26 @@ describe('CardViewDateItemComponent', () => {
format: '',
editable: false
});
component.editable = true;
component.displayEmpty = false;
fixture.detectChanges();
let value = fixture.debugElement.query(By.css('.adf-property-value'));
expect(value).not.toBeNull();
expect(value.nativeElement.innerText.trim()).toBe('');
});
it('should render the default as value if the value is empty, editable:false and displayEmpty is true', () => {
component.property = new CardViewDateItemModel ({
label: 'Date label',
value: '',
key: 'datekey',
default: 'FAKE-DEFAULT-KEY',
format: '',
editable: false
});
component.editable = true;
component.displayEmpty = true;
fixture.detectChanges();
let value = fixture.debugElement.query(By.css('.adf-property-value'));
@ -112,6 +135,7 @@ describe('CardViewDateItemComponent', () => {
format: '',
editable: true
});
component.editable = true;
fixture.detectChanges();
let value = fixture.debugElement.query(By.css('.adf-property-value'));
@ -161,7 +185,7 @@ describe('CardViewDateItemComponent', () => {
expect(datePickerToggle).toBeNull('Datepicker toggle should NOT be shown');
});
it('should open the datetimepicker when clicking on the label', () => {
it('should open the dateXXXpicker when clicking on the label', () => {
component.editable = true;
component.property.editable = true;
fixture.detectChanges();
@ -190,4 +214,20 @@ describe('CardViewDateItemComponent', () => {
component.onDateChanged({value: expectedDate});
});
it('should update the propery\'s value after a succesful update attempt', async(() => {
component.editable = true;
component.property.editable = true;
component.property.value = null;
const expectedDate = moment('Jul 10 2017', 'MMM DD YY');
fixture.detectChanges();
component.onDateChanged({value: expectedDate});
fixture.whenStable().then(
(updateNotification) => {
expect(component.property.value).toEqual(expectedDate.toDate());
}
);
}));
});

View File

@ -16,15 +16,15 @@
*/
import { Component, Input, OnInit, ViewChild } from '@angular/core';
import { MatDatepicker } from '@angular/material';
import { DateAdapter, MAT_DATE_FORMATS } from '@angular/material';
import { MatDatetimepicker } from '@mat-datetimepicker/core';
import moment from 'moment-es6';
import { Moment } from 'moment';
import { CardViewDateItemModel } from '../models/card-view-dateitem.model';
import { CardViewUpdateService } from '../services/card-view-update.service';
import { UserPreferencesService } from '../services/user-preferences.service';
import { MomentDateAdapter } from '../utils/momentDateAdapter';
import { MOMENT_DATE_FORMATS } from '../utils/moment-date-formats.model';
import { CardViewDateItemModel } from '../../models/card-view-dateitem.model';
import { CardViewUpdateService } from '../../services/card-view-update.service';
import { UserPreferencesService } from '../../../services/user-preferences.service';
import { MomentDateAdapter } from '../../../utils/momentDateAdapter';
import { MOMENT_DATE_FORMATS } from '../../../utils/moment-date-formats.model';
@Component({
providers: [
@ -42,10 +42,13 @@ export class CardViewDateItemComponent implements OnInit {
property: CardViewDateItemModel;
@Input()
editable: boolean;
editable: boolean = false;
@ViewChild(MatDatepicker)
public datepicker: MatDatepicker<any>;
@Input()
displayEmpty: boolean = true;
@ViewChild(MatDatetimepicker)
public datepicker: MatDatetimepicker<any>;
valueDate: Moment;
@ -64,7 +67,10 @@ export class CardViewDateItemComponent implements OnInit {
if (this.property.value) {
this.valueDate = moment(this.property.value, this.SHOW_FORMAT);
}
}
showProperty() {
return this.displayEmpty || !this.property.isEmpty();
}
isEditable() {
@ -81,6 +87,7 @@ export class CardViewDateItemComponent implements OnInit {
if (momentDate.isValid()) {
this.valueDate = momentDate;
this.cardViewUpdateService.update(this.property, momentDate.toDate());
this.property.value = momentDate.toDate();
}
}
}

View File

@ -17,14 +17,14 @@
/* tslint:disable:component-selector */
import { Component, Input } from '@angular/core';
import { Component, Input, SimpleChange } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing';
import { CardViewItem } from '../interface/card-view-item.interface';
import { CardItemTypeService } from '../services/card-item-types.service';
import { CardViewContentProxyDirective } from './card-view-content-proxy.directive';
import { CardViewItemDispatcherComponent } from './card-view-item-dispatcher.component';
import { CardViewItem } from '../../interfaces/card-view-item.interface';
import { CardItemTypeService } from '../../services/card-item-types.service';
import { CardViewContentProxyDirective } from '../../directives/card-view-content-proxy.directive';
import { CardViewItemDispatcherComponent } from '../card-view-item-dispatcher/card-view-item-dispatcher.component';
@Component({
selector: 'whatever-you-want-to-have',
@ -79,9 +79,10 @@ describe('CardViewItemDispatcherComponent', () => {
}
};
component.editable = true;
component.displayEmpty = true;
fixture.detectChanges();
component.ngOnChanges(null);
component.ngOnChanges({});
});
afterEach(() => {
@ -97,9 +98,9 @@ describe('CardViewItemDispatcherComponent', () => {
});
it('should load the CardViewShinyCustomElementItemComponent only ONCE', () => {
component.ngOnChanges();
component.ngOnChanges();
component.ngOnChanges();
component.ngOnChanges({});
component.ngOnChanges({});
component.ngOnChanges({});
fixture.detectChanges();
const shinyCustomElementItemComponent = fixture.debugElement.queryAll(By.css('whatever-you-want-to-have'));
@ -107,11 +108,32 @@ describe('CardViewItemDispatcherComponent', () => {
expect(shinyCustomElementItemComponent.length).toEqual(1);
});
it('should pass through the property and editable parameters', () => {
it('should pass through the property, editable and displayEmpty parameters', () => {
const shinyCustomElementItemComponent = fixture.debugElement.query(By.css('whatever-you-want-to-have')).componentInstance;
expect(shinyCustomElementItemComponent.property).toBe(component.property);
expect(shinyCustomElementItemComponent.editable).toBe(component.editable);
expect(shinyCustomElementItemComponent.displayEmpty).toBe(component.displayEmpty);
});
it('should update the subcomponent\'s input parameters', () => {
const expectedEditable = false,
expectedDisplayEmpty = true,
expectedProperty = <CardViewItem> {},
expectedCustomInput = 1;
component.ngOnChanges({
editable: new SimpleChange(true, expectedEditable, false),
displayEmpty: new SimpleChange(false, expectedDisplayEmpty, false),
property: new SimpleChange(null, expectedProperty, false),
customInput: new SimpleChange(0, expectedCustomInput, false)
});
const shinyCustomElementItemComponent = fixture.debugElement.query(By.css('whatever-you-want-to-have')).componentInstance;
expect(shinyCustomElementItemComponent.property).toBe(expectedProperty);
expect(shinyCustomElementItemComponent.editable).toBe(expectedEditable);
expect(shinyCustomElementItemComponent.displayEmpty).toBe(expectedDisplayEmpty);
expect(shinyCustomElementItemComponent.customInput).toBe(expectedCustomInput);
});
});
@ -138,21 +160,21 @@ describe('CardViewItemDispatcherComponent', () => {
lifeCycleMethods.forEach((lifeCycleMethod) => {
shinyCustomElementItemComponent[lifeCycleMethod] = () => {};
spyOn(shinyCustomElementItemComponent, lifeCycleMethod);
const param1 = {};
const param2 = {};
const param = {};
component[lifeCycleMethod].call(component, param1, param2);
component[lifeCycleMethod].call(component, param);
expect(shinyCustomElementItemComponent[lifeCycleMethod]).toHaveBeenCalledWith(param1, param2);
expect(shinyCustomElementItemComponent[lifeCycleMethod]).toHaveBeenCalledWith(param);
});
});
it('should NOT call through the lifecycle methods if the method does not exist (no error should be thrown)', () => {
const param = {};
lifeCycleMethods.forEach((lifeCycleMethod) => {
shinyCustomElementItemComponent[lifeCycleMethod] = undefined;
const execution = () => {
component[lifeCycleMethod].call(component);
component[lifeCycleMethod].call(component, param);
};
expect(execution).not.toThrowError();

View File

@ -20,11 +20,13 @@ import {
ComponentFactoryResolver,
Input,
OnChanges,
SimpleChange,
SimpleChanges,
ViewChild
} from '@angular/core';
import { CardViewItem } from '../interface/card-view-item.interface';
import { CardItemTypeService } from '../services/card-item-types.service';
import { CardViewContentProxyDirective } from './card-view-content-proxy.directive';
import { CardViewItem } from '../../interfaces/card-view-item.interface';
import { CardItemTypeService } from '../../services/card-item-types.service';
import { CardViewContentProxyDirective } from '../../directives/card-view-content-proxy.directive';
@Component({
selector: 'adf-card-view-item-dispatcher',
@ -37,6 +39,9 @@ export class CardViewItemDispatcherComponent implements OnChanges {
@Input()
editable: boolean;
@Input()
displayEmpty: boolean = true;
@ViewChild(CardViewContentProxyDirective)
private content: CardViewContentProxyDirective;
@ -63,13 +68,19 @@ export class CardViewItemDispatcherComponent implements OnChanges {
});
}
ngOnChanges(...args) {
ngOnChanges(changes: SimpleChanges) {
if (!this.loaded) {
this.loadComponent();
this.loaded = true;
}
this.proxy('ngOnChanges', ...args);
Object.keys(changes)
.map(changeName => [changeName, changes[changeName]])
.forEach(([inputParamName, simpleChange]: [string, SimpleChange]) => {
this.componentReference.instance[inputParamName] = simpleChange.currentValue;
});
this.proxy('ngOnChanges', changes);
}
private loadComponent() {
@ -80,6 +91,7 @@ export class CardViewItemDispatcherComponent implements OnChanges {
this.componentReference.instance.editable = this.editable;
this.componentReference.instance.property = this.property;
this.componentReference.instance.displayEmpty = this.displayEmpty;
}
private proxy(methodName, ...args) {

View File

@ -1,12 +1,12 @@
<div class="adf-property-label">{{ property.label | translate }}</div>
<div class="adf-property-label" *ngIf="showProperty()">{{ property.label | translate }}</div>
<div class="adf-property-value">
<div>
<span *ngIf="!isClickable(); else elseBlock" [attr.data-automation-id]="'card-mapitem-value-' + property.key">
<span *ngIf="!property.isEmpty(); else elseEmptyValueBlock">{{ property.displayValue }}</span>
<span *ngIf="showProperty();">{{ property.displayValue }}</span>
</span>
<ng-template #elseBlock>
<span class="adf-mapitem-clickable-value" (click)="clicked()" [attr.data-automation-id]="'card-mapitem-value-' + property.key">
<span *ngIf="!property.isEmpty(); else elseEmptyValueBlock">{{ property.displayValue }}</span>
<span *ngIf="showProperty(); else elseEmptyValueBlock">{{ property.displayValue }}</span>
</span>
</ng-template>
</div>

View File

@ -19,15 +19,15 @@ import { HttpClientModule } from '@angular/common/http';
import { DebugElement } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { MaterialModule } from '../material.module';
import { MaterialModule } from '../../../material.module';
import { By } from '@angular/platform-browser';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { CardViewMapItemModel } from '../models/card-view-mapitem.model';
import { AppConfigService } from '../app-config/app-config.service';
import { CardViewUpdateService } from '../services/card-view-update.service';
import { LogService } from '../services/log.service';
import { TranslateLoaderService } from '../services/translate-loader.service';
import { CardViewMapItemModel } from '../../models/card-view-mapitem.model';
import { AppConfigService } from '../../../app-config/app-config.service';
import { CardViewUpdateService } from '../../services/card-view-update.service';
import { LogService } from '../../../services/log.service';
import { TranslateLoaderService } from '../../../services/translate-loader.service';
import { CardViewMapItemComponent } from './card-view-mapitem.component';
@ -77,14 +77,15 @@ describe('CardViewMapItemComponent', () => {
TestBed.resetTestingModule();
});
it('should render the default if the value is empty', () => {
it('should render the default if the value is empty and displayEmpty is true', () => {
component.property = new CardViewMapItemModel({
label: 'Map label',
value: null,
key: 'mapkey',
default: 'Fake default'
default: 'Fake default',
clickable: false
});
component.displayEmpty = true;
fixture.detectChanges();
let labelValue = debug.query(By.css('.adf-property-label'));
@ -96,6 +97,25 @@ describe('CardViewMapItemComponent', () => {
expect(value.nativeElement.innerText.trim()).toBe('Fake default');
});
it('should NOT render the default if the value is empty and displayEmpty is false', () => {
component.property = new CardViewMapItemModel({
label: 'Map label',
value: null,
key: 'mapkey',
default: 'Fake default',
clickable: false
});
component.displayEmpty = false;
fixture.detectChanges();
let labelValue = debug.query(By.css('.adf-property-label'));
expect(labelValue).toBeNull();
let value = debug.query(By.css(`[data-automation-id="card-mapitem-value-${component.property.key}"]`));
expect(value).not.toBeNull();
expect(value.nativeElement.innerText.trim()).toBe('');
});
it('should render the label and value', () => {
component.property = new CardViewMapItemModel({
label: 'Map label',

View File

@ -16,8 +16,8 @@
*/
import { Component, Input } from '@angular/core';
import { CardViewMapItemModel } from '../models/card-view-mapitem.model';
import { CardViewUpdateService } from '../services/card-view-update.service';
import { CardViewMapItemModel } from '../../models/card-view-mapitem.model';
import { CardViewUpdateService } from '../../services/card-view-update.service';
@Component({
selector: 'adf-card-view-mapitem',
@ -29,8 +29,15 @@ export class CardViewMapItemComponent {
@Input()
property: CardViewMapItemModel;
@Input()
displayEmpty: boolean = true;
constructor(private cardViewUpdateService: CardViewUpdateService) {}
showProperty() {
return this.displayEmpty || !this.property.isEmpty();
}
isClickable() {
return this.property.clickable;
}

View File

@ -0,0 +1,58 @@
<div class="adf-property-label" *ngIf="showProperty() || isEditable()">{{ property.label | translate }}</div>
<div class="adf-property-value">
<span *ngIf="!isEditable()">
<span *ngIf="!isClickable(); else elseBlock" [attr.data-automation-id]="'card-textitem-value-' + property.key">
<span *ngIf="showProperty()">{{ property.displayValue }}</span>
</span>
<ng-template #elseBlock>
<span class="adf-textitem-clickable-value" (click)="clicked()" [attr.data-automation-id]="'card-textitem-value-' + property.key">
<span *ngIf="showProperty(); else elseEmptyValueBlock">{{ property.displayValue }}</span>
</span>
</ng-template>
</span>
<span *ngIf="isEditable()">
<div *ngIf="!inEdit" (click)="setEditMode(true)" class="adf-textitem-readonly" [attr.data-automation-id]="'card-textitem-edit-toggle-' + property.key" fxLayout="row" fxLayoutAlign="space-between center">
<span [attr.data-automation-id]="'card-textitem-value-' + property.key">
<span *ngIf="showProperty(); else elseEmptyValueBlock">{{ property.displayValue }}</span>
</span>
<mat-icon fxFlex="0 0 auto" [attr.data-automation-id]="'card-textitem-edit-icon-' + property.key" class="adf-textitem-icon">create</mat-icon>
</div>
<div *ngIf="inEdit" class="adf-textitem-editable">
<div class="adf-textitem-editable-controls">
<mat-form-field floatPlaceholder="never" class="adf-input-container">
<input *ngIf="!property.multiline" #editorInput
matInput
class="adf-input"
[placeholder]="property.default | translate"
[(ngModel)]="editedValue"
[attr.data-automation-id]="'card-textitem-editinput-' + property.key">
<textarea *ngIf="property.multiline" #editorInput
matInput
matTextareaAutosize
matAutosizeMaxRows="1"
matAutosizeMaxRows="5"
class="adf-textarea"
[placeholder]="property.default | translate"
[(ngModel)]="editedValue"
[attr.data-automation-id]="'card-textitem-edittextarea-' + property.key"></textarea>
</mat-form-field>
<mat-icon
class="adf-textitem-icon adf-update-icon"
(click)="update()"
[attr.data-automation-id]="'card-textitem-update-' + property.key">done</mat-icon>
<mat-icon
class="adf-textitem-icon adf-reset-icon"
(click)="reset()"
[attr.data-automation-id]="'card-textitem-reset-' + property.key">clear</mat-icon>
</div>
<mat-error class="adf-textitem-editable-error" *ngIf="hasErrors()">
<ul>
<li *ngFor="let errorMessage of errorMessages">{{ errorMessage | translate }}</li>
</ul>
</mat-error>
</div>
</span>
<ng-template #elseEmptyValueBlock>
<span class="adf-textitem-default-value">{{ property.default | translate }}</span>
</ng-template>
</div>

View File

@ -30,6 +30,8 @@
}
&-textitem-editable {
&-controls {
display: flex;
mat-icon:hover {
@ -47,6 +49,27 @@
}
}
&-error {
font-size: 12px;
padding-top: 4px;
ul {
margin: 0;
padding: 0;
list-style-type: none;
li {
margin: 0;
padding: 0;
}
}
}
}
&-textitem-default-value {
color: mat-color($foreground, text, 0.54);
}
&-textitem-editable .mat-input-wrapper {
margin: 0;
padding-bottom: 0;
@ -67,7 +90,7 @@
}
&-textitem-editable .mat-input-placeholder {
top: 0;
top: 4px;
}
&-textitem-editable .mat-input-element {

View File

@ -0,0 +1,325 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* 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 { HttpClientModule } from '@angular/common/http';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { MatDatepickerModule, MatIconModule, MatInputModule, MatNativeDateModule } from '@angular/material';
import { By } from '@angular/platform-browser';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { CardViewTextItemModel } from '../../models/card-view-textitem.model';
import { AppConfigService } from '../../../app-config/app-config.service';
import { CardViewUpdateService } from '../../services/card-view-update.service';
import { LogService } from '../../../services/log.service';
import { TranslateLoaderService } from '../../../services/translate-loader.service';
import { CardViewTextItemComponent } from './card-view-textitem.component';
describe('CardViewTextItemComponent', () => {
let fixture: ComponentFixture<CardViewTextItemComponent>;
let component: CardViewTextItemComponent;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
HttpClientModule,
FormsModule,
NoopAnimationsModule,
MatDatepickerModule,
MatIconModule,
MatInputModule,
MatNativeDateModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: TranslateLoaderService
}
})
],
declarations: [
CardViewTextItemComponent
],
providers: [
AppConfigService,
CardViewUpdateService,
LogService
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CardViewTextItemComponent);
component = fixture.componentInstance;
component.property = new CardViewTextItemModel ({
label: 'Text label',
value: 'Lorem ipsum',
key: 'textkey',
default: 'FAKE-DEFAULT-KEY',
editable: false
});
});
afterEach(() => {
fixture.destroy();
TestBed.resetTestingModule();
});
describe('Rendering', () => {
it('should render the label and value', () => {
fixture.detectChanges();
let labelValue = fixture.debugElement.query(By.css('.adf-property-label'));
expect(labelValue).not.toBeNull();
expect(labelValue.nativeElement.innerText).toBe('Text label');
let value = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-value-${component.property.key}"]`));
expect(value).not.toBeNull();
expect(value.nativeElement.innerText.trim()).toBe('Lorem ipsum');
});
it('should NOT render the default as value if the value is empty, editable is false and displayEmpty is false', () => {
component.property = new CardViewTextItemModel ({
label: 'Text label',
value: '',
key: 'textkey',
default: 'FAKE-DEFAULT-KEY',
editable: false
});
component.displayEmpty = false;
fixture.detectChanges();
let value = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-value-${component.property.key}"]`));
expect(value).not.toBeNull();
expect(value.nativeElement.innerText.trim()).toBe('');
});
it('should render the default as value if the value is empty, editable is false and displayEmpty is true', () => {
component.property = new CardViewTextItemModel ({
label: 'Text label',
value: '',
key: 'textkey',
default: 'FAKE-DEFAULT-KEY',
editable: false
});
component.displayEmpty = true;
fixture.detectChanges();
let value = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-value-${component.property.key}"]`));
expect(value).not.toBeNull();
expect(value.nativeElement.innerText.trim()).toBe('FAKE-DEFAULT-KEY');
});
it('should render the default as value if the value is empty and editable true', () => {
component.property = new CardViewTextItemModel ({
label: 'Text label',
value: '',
key: 'textkey',
default: 'FAKE-DEFAULT-KEY',
editable: true
});
component.editable = true;
fixture.detectChanges();
let value = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-value-${component.property.key}"]`));
expect(value).not.toBeNull();
expect(value.nativeElement.innerText.trim()).toBe('FAKE-DEFAULT-KEY');
});
it('should NOT render the default as value if the value is empty, clickable is false and displayEmpty is false', () => {
component.property = new CardViewTextItemModel ({
label: 'Text label',
value: '',
key: 'textkey',
default: 'FAKE-DEFAULT-KEY',
clickable: false
});
component.displayEmpty = false;
fixture.detectChanges();
let value = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-value-${component.property.key}"]`));
expect(value).not.toBeNull();
expect(value.nativeElement.innerText.trim()).toBe('');
});
it('should render the default as value if the value is empty, clickable is false and displayEmpty is true', () => {
component.property = new CardViewTextItemModel ({
label: 'Text label',
value: '',
key: 'textkey',
default: 'FAKE-DEFAULT-KEY',
clickable: false
});
component.displayEmpty = true;
fixture.detectChanges();
let value = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-value-${component.property.key}"]`));
expect(value).not.toBeNull();
expect(value.nativeElement.innerText.trim()).toBe('FAKE-DEFAULT-KEY');
});
it('should render the default as value if the value is empty and clickable true', () => {
component.property = new CardViewTextItemModel ({
label: 'Text label',
value: '',
key: 'textkey',
default: 'FAKE-DEFAULT-KEY',
clickable: true
});
fixture.detectChanges();
let value = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-value-${component.property.key}"]`));
expect(value).not.toBeNull();
expect(value.nativeElement.innerText.trim()).toBe('FAKE-DEFAULT-KEY');
});
it('should render value when editable:true', () => {
component.editable = true;
component.property.editable = true;
fixture.detectChanges();
let value = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-value-${component.property.key}"]`));
expect(value).not.toBeNull();
expect(value.nativeElement.innerText.trim()).toBe('Lorem ipsum');
});
it('should render the edit icon in case of editable:true', () => {
component.editable = true;
component.property.editable = true;
fixture.detectChanges();
let editIcon = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-edit-icon-${component.property.key}"]`));
expect(editIcon).not.toBeNull('Edit icon should be shown');
});
it('should NOT render the edit icon in case of editable:false', () => {
component.editable = false;
fixture.detectChanges();
let editIcon = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-edit-icon-${component.property.key}"]`));
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', () => {
component.editable = false;
component.property.editable = true;
fixture.detectChanges();
let editIcon = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-edit-icon-${component.property.key}"]`));
expect(editIcon).toBeNull('Edit icon should NOT be shown');
});
});
describe('Update', () => {
beforeEach(() => {
component.editable = true;
component.property.editable = true;
component.inEdit = true;
component.editedValue = 'updated-value';
fixture.detectChanges();
});
it('should call the isValid method with the edited value', () => {
spyOn(component.property, 'isValid');
component.editedValue = 'updated-value';
component.update();
expect(component.property.isValid).toHaveBeenCalledWith('updated-value');
});
it('should trigger the update event if the editedValue is valid', () => {
component.property.isValid = () => true;
const cardViewUpdateService = TestBed.get(CardViewUpdateService);
spyOn(cardViewUpdateService, 'update');
component.editedValue = 'updated-value';
component.update();
expect(cardViewUpdateService.update).toHaveBeenCalledWith(component.property, 'updated-value');
});
it('should NOT trigger the update event if the editedValue is invalid', () => {
component.property.isValid = () => false;
const cardViewUpdateService = TestBed.get(CardViewUpdateService);
spyOn(cardViewUpdateService, 'update');
component.update();
expect(cardViewUpdateService.update).not.toHaveBeenCalled();
});
it('should set the errorMessages properly if the editedValue is invalid', () => {
const expectedErrorMessages = ['Something went wrong'];
component.property.isValid = () => false;
component.property.getValidationErrors = () => expectedErrorMessages;
component.update();
expect(component.errorMessages).toBe(expectedErrorMessages);
});
it('should update the propery\'s value after a succesful update attempt', async(() => {
component.property.isValid = () => true;
component.update();
fixture.whenStable().then(() => {
expect(component.property.value).toBe(component.editedValue);
});
}));
it('should switch back to readonly mode after an update attempt', async(() => {
component.property.isValid = () => true;
component.update();
fixture.whenStable().then(() => {
expect(component.inEdit).toBeFalsy();
});
}));
it('should trigger an update event on the CardViewUpdateService [integration]', (done) => {
component.inEdit = false;
component.property.isValid = () => true;
const cardViewUpdateService = TestBed.get(CardViewUpdateService);
const expectedText = 'changed text';
fixture.detectChanges();
cardViewUpdateService.itemUpdated$.subscribe(
(updateNotification) => {
expect(updateNotification.target).toBe(component.property);
expect(updateNotification.changed).toEqual({ textkey: expectedText });
done();
}
);
let editIcon = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-edit-toggle-${component.property.key}"]`));
editIcon.triggerEventHandler('click', null);
fixture.detectChanges();
let editInput = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-editinput-${component.property.key}"]`));
editInput.nativeElement.value = expectedText;
editInput.nativeElement.dispatchEvent(new Event('input'));
fixture.detectChanges();
let updateInput = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-update-${component.property.key}"]`));
updateInput.triggerEventHandler('click', null);
});
});
});

View File

@ -16,8 +16,8 @@
*/
import { Component, Input, OnChanges, ViewChild } from '@angular/core';
import { CardViewTextItemModel } from '../models/card-view-textitem.model';
import { CardViewUpdateService } from '../services/card-view-update.service';
import { CardViewTextItemModel } from '../../models/card-view-textitem.model';
import { CardViewUpdateService } from '../../services/card-view-update.service';
@Component({
selector: 'adf-card-view-textitem',
@ -29,28 +29,40 @@ export class CardViewTextItemComponent implements OnChanges {
property: CardViewTextItemModel;
@Input()
editable: boolean;
editable: boolean = false;
@Input()
displayEmpty: boolean = true;
@ViewChild('editorInput')
private editorInput: any;
inEdit: boolean = false;
editedValue: string;
errorMessages: string[];
constructor(private cardViewUpdateService: CardViewUpdateService) {}
ngOnChanges() {
ngOnChanges(): void {
this.editedValue = this.property.value;
}
isEditable() {
showProperty(): boolean {
return this.displayEmpty || !this.property.isEmpty();
}
isEditable(): boolean {
return this.editable && this.property.editable;
}
isClickable() {
isClickable(): boolean {
return this.property.clickable;
}
hasErrors(): number {
return this.errorMessages && this.errorMessages.length;
}
setEditMode(editStatus: boolean): void {
this.inEdit = editStatus;
setTimeout(() => {
@ -66,9 +78,13 @@ export class CardViewTextItemComponent implements OnChanges {
}
update(): void {
if (this.property.isValid(this.editedValue)) {
this.cardViewUpdateService.update(this.property, this.editedValue );
this.property.value = this.editedValue;
this.setEditMode(false);
} else {
this.errorMessages = this.property.getValidationErrors(this.editedValue);
}
}
clicked(): void {

View File

@ -0,0 +1,23 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './card-view/card-view.component';
export * from './card-view-boolitem/card-view-boolitem.component';
export * from './card-view-dateitem/card-view-dateitem.component';
export * from './card-view-item-dispatcher/card-view-item-dispatcher.component';
export * from './card-view-mapitem/card-view-mapitem.component';
export * from './card-view-textitem/card-view-textitem.component';

View File

@ -0,0 +1,11 @@
<div class="adf-property-list">
<div *ngFor="let property of properties">
<div [attr.data-automation-id]="'header-'+property.key" class="adf-property">
<adf-card-view-item-dispatcher
[property]="property"
[editable]="editable"
[displayEmpty]="displayEmpty">
</adf-card-view-item-dispatcher>
</div>
</div>
</div>

View File

@ -0,0 +1,22 @@
@mixin adf-card-view-theme($theme) {
$primary: map-get($theme, primary);
$foreground: map-get($theme, foreground);
.adf-property-list {
.adf-property {
margin-bottom: 20px;
.adf-property-label {
font-size: 12px;
color: mat-color($foreground, text, 0.4);
word-wrap: break-word;
}
.adf-property-value {
font-size: 14px;
color: mat-color($foreground, text, 0.87);
}
}
}
}

View File

@ -20,24 +20,26 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { HttpClientModule } from '@angular/common/http';
import { FormsModule } from '@angular/forms';
import { MatDatepickerModule, MatIconModule, MatInputModule, MatNativeDateModule } from '@angular/material';
import { MatDatetimepickerModule, MatNativeDatetimeModule } from '@mat-datetimepicker/core';
import { By } from '@angular/platform-browser';
import { BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { AppConfigService } from '../app-config/app-config.service';
import { AppConfigService } from '../../../app-config/app-config.service';
import { CardViewDateItemModel } from '../models/card-view-dateitem.model';
import { CardViewTextItemModel } from '../models/card-view-textitem.model';
import { CardViewUpdateService } from '../services/card-view-update.service';
import { CardViewDateItemModel } from '../../models/card-view-dateitem.model';
import { CardViewTextItemModel } from '../../models/card-view-textitem.model';
import { CardViewUpdateService } from '../../services/card-view-update.service';
import { TranslateLoaderService } from '../services/translate-loader.service';
import { CardViewContentProxyDirective } from './card-view-content-proxy.directive';
import { CardViewDateItemComponent } from './card-view-dateitem.component';
import { CardViewItemDispatcherComponent } from './card-view-item-dispatcher.component';
import { CardViewTextItemComponent } from './card-view-textitem.component';
import { TranslateLoaderService } from '../../../services/translate-loader.service';
import { CardViewContentProxyDirective } from '../../directives/card-view-content-proxy.directive';
import { CardViewDateItemComponent } from '../card-view-dateitem/card-view-dateitem.component';
import { CardItemTypeService } from '../../services/card-item-types.service';
import { CardViewItemDispatcherComponent } from '../card-view-item-dispatcher/card-view-item-dispatcher.component';
import { CardViewTextItemComponent } from '../card-view-textitem/card-view-textitem.component';
import { CardViewComponent } from './card-view.component';
describe('AdfCardView', () => {
describe('CardViewComponent', () => {
let fixture: ComponentFixture<CardViewComponent>;
let component: CardViewComponent;
@ -50,6 +52,8 @@ describe('AdfCardView', () => {
MatIconModule,
MatInputModule,
MatNativeDateModule,
MatDatetimepickerModule,
MatNativeDatetimeModule,
FormsModule,
TranslateModule.forRoot({
loader: {
@ -66,6 +70,7 @@ describe('AdfCardView', () => {
CardViewDateItemComponent
],
providers: [
CardItemTypeService,
CardViewUpdateService,
AppConfigService
]
@ -135,14 +140,42 @@ describe('AdfCardView', () => {
});
}));
it('should render the default value if the value is empty', async(() => {
it('should NOT render anything if the value is empty, not editable and displayEmpty is false', async(() => {
component.properties = [new CardViewTextItemModel({
label: 'My default label',
value: null,
default: 'default value',
key: 'some key'
key: 'some-key',
editable: false
})];
component.editable = true;
component.displayEmpty = false;
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
let labelValue = fixture.debugElement.query(By.css('.adf-property-label'));
expect(labelValue).toBeNull();
let value = fixture.debugElement.query(By.css('.adf-property-value'));
expect(value).not.toBeNull();
expect(value.nativeElement.innerText.trim()).toBe('');
});
}));
it('should render the default value if the value is empty, not editable and displayEmpty is true', async(() => {
component.properties = [new CardViewTextItemModel({
label: 'My default label',
value: null,
default: 'default value',
key: 'some-key',
editable: false
})];
component.editable = true;
component.displayEmpty = true;
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
@ -150,9 +183,34 @@ describe('AdfCardView', () => {
expect(labelValue).not.toBeNull();
expect(labelValue.nativeElement.innerText).toBe('My default label');
let value = fixture.debugElement.query(By.css('.adf-property-value'));
let value = fixture.debugElement.query(By.css('.adf-property-value [data-automation-id="card-textitem-value-some-key"]'));
expect(value).not.toBeNull();
expect(value.nativeElement.innerText).toBe('default value');
expect(value.nativeElement.innerText.trim()).toBe('default value');
});
}));
it('should render the default value if the value is empty and is editable', async(() => {
component.properties = [new CardViewTextItemModel({
label: 'My default label',
value: null,
default: 'default value',
key: 'some-key',
editable: true
})];
component.editable = true;
component.displayEmpty = false;
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
let labelValue = fixture.debugElement.query(By.css('.adf-property-label'));
expect(labelValue).not.toBeNull();
expect(labelValue.nativeElement.innerText).toBe('My default label');
let value = fixture.debugElement.query(By.css('.adf-property-value [data-automation-id="card-textitem-value-some-key"]'));
expect(value).not.toBeNull();
expect(value.nativeElement.innerText.trim()).toBe('default value');
});
}));
});

View File

@ -16,7 +16,7 @@
*/
import { Component, Input } from '@angular/core';
import { CardViewItem } from '../interface/card-view-item.interface';
import { CardViewItem } from '../../interfaces/card-view-item.interface';
@Component({
selector: 'adf-card-view',
@ -29,4 +29,7 @@ export class CardViewComponent {
@Input()
editable: boolean;
@Input()
displayEmpty: boolean = true;
}

View File

@ -0,0 +1,23 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* 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 { CardViewItemProperties } from './card-view-item-properties.interface';
export interface CardViewBoolItemProperties extends CardViewItemProperties {
value: any;
default?: boolean;
}

View File

@ -0,0 +1,22 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* 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 { CardViewItemProperties } from './card-view-item-properties.interface';
export interface CardViewDateItemProperties extends CardViewItemProperties {
format?: string;
}

View File

@ -0,0 +1,28 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* 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 { CardViewItemValidator } from './card-view-item-validator.interface';
export interface CardViewItemProperties {
label: string;
value: any;
key: any;
default?: any;
editable?: boolean;
clickable?: boolean;
validators?: CardViewItemValidator[];
}

View File

@ -0,0 +1,21 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export interface CardViewItemValidator {
message: string;
isValid(value: any): boolean;
}

View File

@ -21,6 +21,6 @@ export interface CardViewItem {
key: string;
default?: any;
type: string;
displayValue: string;
displayValue: any;
editable?: boolean;
}

View File

@ -0,0 +1,23 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* 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 { PipeTransform } from '@angular/core';
export interface CardViewTextItemPipeProperty {
pipe: PipeTransform;
params?: any[];
}

View File

@ -0,0 +1,24 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* 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 { CardViewItemProperties } from './card-view-item-properties.interface';
import { CardViewTextItemPipeProperty } from './card-view-textitem-pipe-property.interface';
export interface CardViewTextItemProperties extends CardViewItemProperties {
multiline?: boolean;
pipes?: CardViewTextItemPipeProperty[];
}

View File

@ -0,0 +1,24 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './card-view-item-properties.interface';
export * from './card-view-item-validator.interface';
export * from './card-view-item.interface';
export * from './card-view-textitem-properties.interface';
export * from './card-view-dateitem-properties.interface';
export * from './card-view-boolitem-properties.interface';
export * from './card-view-textitem-pipe-property.interface';

View File

@ -0,0 +1,85 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* 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 { CardViewItemProperties } from '../interfaces/card-view.interfaces';
import { CardViewBaseItemModel } from './card-view-baseitem.model';
import { CardViewItemValidator } from '../interfaces/card-view.interfaces';
class CarViewCustomItemModel extends CardViewBaseItemModel {}
describe('CardViewBaseItemModel', () => {
let properties: CardViewItemProperties;
beforeEach(() => {
properties = {
label: 'Tribe',
value: 'Oseram',
key: 'tribe'
};
});
describe('isValid & Validation errors', () => {
it('should be true when no validators are set', () => {
const itemModel = new CarViewCustomItemModel(properties);
const isValid = itemModel.isValid(null);
expect(isValid).toBe(true);
});
it('should call the registered validators to validate', () => {
const validator1: CardViewItemValidator = { isValid: () => true, message: 'validator 1' };
const validator2: CardViewItemValidator = { isValid: () => true, message: 'validator 2' };
spyOn(validator1, 'isValid');
spyOn(validator2, 'isValid');
properties.validators = [ validator1, validator2 ];
const itemModel = new CarViewCustomItemModel(properties);
itemModel.isValid('test-against-this');
expect(validator1.isValid).toHaveBeenCalledWith('test-against-this');
expect(validator2.isValid).toHaveBeenCalledWith('test-against-this');
});
it('should return the registered validators\' common decision (case true)', () => {
const validator1: CardViewItemValidator = { isValid: () => true, message: 'validator 1' };
const validator2: CardViewItemValidator = { isValid: () => true, message: 'validator 2' };
properties.validators = [ validator1, validator2 ];
const itemModel = new CarViewCustomItemModel(properties);
const isValid = itemModel.isValid('test-against-this');
expect(isValid).toBe(true);
expect(itemModel.getValidationErrors('test-against-this')).toEqual([]);
});
it('should return the registered validators\' common decision (case false)', () => {
const validator1: CardViewItemValidator = { isValid: () => false, message: 'validator 1' };
const validator2: CardViewItemValidator = { isValid: () => true, message: 'validator 2' };
const validator3: CardViewItemValidator = { isValid: () => false, message: 'validator 3' };
properties.validators = [ validator1, validator2, validator3 ];
const itemModel = new CarViewCustomItemModel(properties);
const isValid = itemModel.isValid('test-against-this');
expect(isValid).toBe(false);
expect(itemModel.getValidationErrors('test-against-this')).toEqual(['validator 1', 'validator 3']);
});
});
});

View File

@ -15,30 +15,16 @@
* limitations under the License.
*/
/**
*
* This object represent the basic structure of a card view.
*
*
* @returns {CardViewBaseItemModel} .
*/
export interface CardViewItemProperties {
label: string;
value: any;
key: any;
default?: string;
editable?: boolean;
clickable?: boolean;
}
import { CardViewItemProperties, CardViewItemValidator } from '../interfaces/card-view.interfaces';
export abstract class CardViewBaseItemModel {
label: string;
value: any;
key: any;
default: string;
default: any;
editable: boolean;
clickable: boolean;
validators?: CardViewItemValidator[];
constructor(obj: CardViewItemProperties) {
this.label = obj.label || '';
@ -47,9 +33,30 @@ export abstract class CardViewBaseItemModel {
this.default = obj.default;
this.editable = !!obj.editable;
this.clickable = !!obj.clickable;
this.validators = obj.validators || [];
}
isEmpty(): boolean {
return this.value === undefined || this.value === null || this.value === '';
}
isValid(newValue: any): boolean {
if (!this.validators.length) {
return true;
}
return this.validators
.map((validator) => validator.isValid(newValue))
.reduce((isValidUntilNow, isValid) => isValidUntilNow && isValid, true);
}
getValidationErrors(value): string[] {
if (!this.validators.length) {
return [];
}
return this.validators
.filter((validator) => !validator.isValid(value))
.map((validator) => validator.message);
}
}

View File

@ -0,0 +1,88 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* 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 { CardViewBoolItemModel } from './card-view-boolitem.model';
import { CardViewBoolItemProperties } from '../interfaces/card-view.interfaces';
describe('CardViewFloatItemModel', () => {
let properties: CardViewBoolItemProperties;
beforeEach(() => {
properties = {
label: 'Tribe',
value: undefined,
key: 'tribe'
};
});
it('true should be parsed as true', () => {
properties.value = true;
const itemModel = new CardViewBoolItemModel(properties);
expect(itemModel.value).toBe(true);
});
it('"true" should be parsed as true', () => {
properties.value = 'true';
const itemModel = new CardViewBoolItemModel(properties);
expect(itemModel.value).toBe(true);
});
it('1 should be parsed as true', () => {
properties.value = 1;
const itemModel = new CardViewBoolItemModel(properties);
expect(itemModel.value).toBe(true);
});
it('"1" should be parsed as true', () => {
properties.value = '1';
const itemModel = new CardViewBoolItemModel(properties);
expect(itemModel.value).toBe(true);
});
it('"false" should be parsed as false', () => {
properties.value = 'false';
const itemModel = new CardViewBoolItemModel(properties);
expect(itemModel.value).toBe(false);
});
it('false should be parsed as false', () => {
properties.value = 'false';
const itemModel = new CardViewBoolItemModel(properties);
expect(itemModel.value).toBe(false);
});
it('undefined should be parsed as false', () => {
properties.value = undefined;
const itemModel = new CardViewBoolItemModel(properties);
expect(itemModel.value).toBe(false);
});
it('null should be parsed as false', () => {
properties.value = null;
const itemModel = new CardViewBoolItemModel(properties);
expect(itemModel.value).toBe(false);
});
});

View File

@ -0,0 +1,43 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* 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 { CardViewItem } from '../interfaces/card-view-item.interface';
import { DynamicComponentModel } from '../../services/dynamic-component-mapper.service';
import { CardViewBaseItemModel } from './card-view-baseitem.model';
import { CardViewBoolItemProperties } from '../interfaces/card-view.interfaces';
export class CardViewBoolItemModel extends CardViewBaseItemModel implements CardViewItem, DynamicComponentModel {
type: string = 'bool';
value: boolean = false;
default: boolean;
constructor(obj: CardViewBoolItemProperties) {
super(obj);
if (obj.value !== undefined) {
this.value = !!JSON.parse(obj.value);
}
}
get displayValue() {
if (this.isEmpty()) {
return this.default;
} else {
return this.value;
}
}
}

View File

@ -15,30 +15,23 @@
* limitations under the License.
*/
/**
*
* This object represent the basic structure of a card view.
*
*
* @returns {CardViewDateItemModel} .
*/
import moment from 'moment-es6';
import { CardViewItem } from '../interface/card-view-item.interface';
import { DynamicComponentModel } from '../services/dynamic-component-mapper.service';
import { CardViewBaseItemModel, CardViewItemProperties } from './card-view-baseitem.model';
export interface CardViewDateItemProperties extends CardViewItemProperties {
format?: string;
}
import { CardViewItem } from '../interfaces/card-view-item.interface';
import { DynamicComponentModel } from '../../services/dynamic-component-mapper.service';
import { CardViewBaseItemModel } from './card-view-baseitem.model';
import { CardViewDateItemProperties } from '../interfaces/card-view.interfaces';
export class CardViewDateItemModel extends CardViewBaseItemModel implements CardViewItem, DynamicComponentModel {
type: string = 'date';
format: string;
format: string = 'MMM DD YYYY';
constructor(obj: CardViewDateItemProperties) {
super(obj);
this.format = obj.format || 'MMM DD YYYY';
if (obj.format) {
this.format = obj.format;
}
}
get displayValue() {

View File

@ -0,0 +1,25 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* 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 { CardViewItem } from '../interfaces/card-view-item.interface';
import { DynamicComponentModel } from '../../services/dynamic-component-mapper.service';
import { CardViewDateItemModel } from './card-view-dateitem.model';
export class CardViewDatetimeItemModel extends CardViewDateItemModel implements CardViewItem, DynamicComponentModel {
type: string = 'datetime';
format: string = 'MMM DD YYYY HH:mm';
}

View File

@ -0,0 +1,59 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* 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 { CardViewFloatItemModel } from './card-view-floatitem.model';
import { CardViewTextItemProperties } from '../interfaces/card-view.interfaces';
describe('CardViewFloatItemModel', () => {
let properties: CardViewTextItemProperties;
beforeEach(() => {
properties = {
label: 'Tribe',
value: '42.42',
key: 'tribe'
};
});
it('value should be parsed as float', () => {
const itemModel = new CardViewFloatItemModel(properties);
expect(itemModel.value).toBe(42.42);
});
it('value should be parsed as float only if there is a value', () => {
properties.value = undefined;
const itemModel = new CardViewFloatItemModel(properties);
expect(itemModel.value).toBe(undefined);
});
it('isValid should return the validator\'s value', () => {
const itemModel = new CardViewFloatItemModel(properties);
expect(itemModel.isValid(42)).toBe(true, 'For 42 it should be true');
expect(itemModel.isValid(42.0)).toBe(true, 'For 42.0 it should be true');
expect(itemModel.isValid('42')).toBe(true, 'For "42" it should be true');
expect(itemModel.isValid('42.0')).toBe(true, 'For "42.0" it should be true');
expect(itemModel.isValid('4e2')).toBe(true, 'For "4e2" it should be true');
expect(itemModel.isValid('4g2')).toBe(false, 'For "4g2" it should be false');
expect(itemModel.isValid(42.3)).toBe(true, 'For 42.3 it should be true');
expect(itemModel.isValid('42.3')).toBe(true, 'For "42.3" it should be true');
expect(itemModel.isValid('test')).toBe(false, 'For "test" it should be false');
});
});

View File

@ -0,0 +1,35 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* 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 { CardViewItem } from '../interfaces/card-view-item.interface';
import { DynamicComponentModel } from '../../services/dynamic-component-mapper.service';
import { CardViewTextItemModel } from './card-view-textitem.model';
import { CardViewTextItemProperties } from '../interfaces/card-view.interfaces';
import { CardViewItemFloatValidator } from '..//validators/card-view.validators';
export class CardViewFloatItemModel extends CardViewTextItemModel implements CardViewItem, DynamicComponentModel {
type: string = 'float';
constructor(obj: CardViewTextItemProperties) {
super(obj);
this.validators.push(new CardViewItemFloatValidator());
if (obj.value) {
this.value = parseFloat(obj.value);
}
}
}

View File

@ -0,0 +1,59 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* 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 { CardViewIntItemModel } from './card-view-intitem.model';
import { CardViewTextItemProperties } from '../interfaces/card-view.interfaces';
describe('CardViewIntItemModel', () => {
let properties: CardViewTextItemProperties;
beforeEach(() => {
properties = {
label: 'Tribe',
value: '42',
key: 'tribe'
};
});
it('value should be parsed as integer', () => {
const itemModel = new CardViewIntItemModel(properties);
expect(itemModel.value).toBe(42);
});
it('value should be parsed as integer only if there is a value', () => {
properties.value = undefined;
const itemModel = new CardViewIntItemModel(properties);
expect(itemModel.value).toBe(undefined);
});
it('isValid should return the validator\'s value', () => {
const itemModel = new CardViewIntItemModel(properties);
expect(itemModel.isValid(42)).toBe(true, 'For 42 it should be true');
expect(itemModel.isValid(42.0)).toBe(true, 'For 42.0 it should be true');
expect(itemModel.isValid('42')).toBe(true, 'For "42" it should be true');
expect(itemModel.isValid('42.0')).toBe(true, 'For "42.0" it should be true');
expect(itemModel.isValid('4e2')).toBe(true, 'For "4e2" it should be true');
expect(itemModel.isValid('4g2')).toBe(false, 'For "4g2" it should be false');
expect(itemModel.isValid(42.3)).toBe(false, 'For 42.3 it should be false');
expect(itemModel.isValid('42.3')).toBe(false, 'For "42.3" it should be false');
expect(itemModel.isValid('test')).toBe(false, 'For "test" it should be false');
});
});

View File

@ -0,0 +1,35 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* 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 { CardViewItem } from '../interfaces/card-view-item.interface';
import { DynamicComponentModel } from '../../services/dynamic-component-mapper.service';
import { CardViewTextItemModel } from './card-view-textitem.model';
import { CardViewTextItemProperties } from '../interfaces/card-view.interfaces';
import { CardViewItemIntValidator } from '../validators/card-view.validators';
export class CardViewIntItemModel extends CardViewTextItemModel implements CardViewItem, DynamicComponentModel {
type: string = 'int';
constructor(obj: CardViewTextItemProperties) {
super(obj);
this.validators.push(new CardViewItemIntValidator());
if (obj.value) {
this.value = parseInt(obj.value, 10);
}
}
}

View File

@ -15,26 +15,14 @@
* limitations under the License.
*/
/**
*
* This object represent the basic structure of a card view.
*
*
* @returns {CardViewMapItemModel} .
*/
import { CardViewItem } from '../interface/card-view-item.interface';
import { DynamicComponentModel } from '../services/dynamic-component-mapper.service';
import { CardViewBaseItemModel, CardViewItemProperties } from './card-view-baseitem.model';
import { CardViewItem } from '../interfaces/card-view-item.interface';
import { DynamicComponentModel } from '../../services/dynamic-component-mapper.service';
import { CardViewBaseItemModel } from './card-view-baseitem.model';
export class CardViewMapItemModel extends CardViewBaseItemModel implements CardViewItem, DynamicComponentModel {
type: string = 'map';
value: Map<string, string>;
constructor(obj: CardViewItemProperties) {
super(obj);
}
get displayValue() {
if (this.value && this.value.size > 0) {
return this.value.values().next().value;

View File

@ -16,7 +16,8 @@
*/
import { PipeTransform } from '@angular/core';
import { CardViewTextItemModel, CardViewTextItemProperties } from './card-view-textitem.model';
import { CardViewTextItemModel } from './card-view-textitem.model';
import { CardViewTextItemProperties } from '../interfaces/card-view.interfaces';
class TestPipe implements PipeTransform {
transform(value: string, pipeParam: string): string {
@ -39,28 +40,36 @@ describe('CardViewTextItemModel', () => {
describe('displayValue', () => {
it('should return the extension if file has it', () => {
const file = new CardViewTextItemModel(properties);
it('should return the value if it is present', () => {
const itemModel = new CardViewTextItemModel(properties);
expect(file.displayValue).toBe('Banuk');
expect(itemModel.displayValue).toBe('Banuk');
});
it('should return the default value if the value is not present', () => {
properties.value = undefined;
properties.default = 'default-value';
const itemModel = new CardViewTextItemModel(properties);
expect(itemModel.displayValue).toBe('default-value');
});
it('should apply a pipe on the value if it is present', () => {
properties.pipes = [
{ pipe: new TestPipe() }
];
const file = new CardViewTextItemModel(properties);
const itemModel = new CardViewTextItemModel(properties);
expect(file.displayValue).toBe('testpiped-Banuk');
expect(itemModel.displayValue).toBe('testpiped-Banuk');
});
it('should apply a pipe on the value with parameters if those are present', () => {
properties.pipes = [
{ pipe: new TestPipe(), params: ['withParams'] }
];
const file = new CardViewTextItemModel(properties);
const itemModel = new CardViewTextItemModel(properties);
expect(file.displayValue).toBe('testpiped-Banuk-withParams');
expect(itemModel.displayValue).toBe('testpiped-Banuk-withParams');
});
it('should apply more pipes on the value with parameters if those are present', () => {
@ -70,9 +79,9 @@ describe('CardViewTextItemModel', () => {
{ pipe, params: ['2'] },
{ pipe, params: ['3'] }
];
const file = new CardViewTextItemModel(properties);
const itemModel = new CardViewTextItemModel(properties);
expect(file.displayValue).toBe('testpiped-testpiped-testpiped-Banuk-1-2-3');
expect(itemModel.displayValue).toBe('testpiped-testpiped-testpiped-Banuk-1-2-3');
});
});
});

View File

@ -15,31 +15,15 @@
* limitations under the License.
*/
/**
*
* This object represent the basic structure of a card view.
*
*
* @returns {CardViewTextItemModel} .
*/
import { CardViewItem } from '../interfaces/card-view-item.interface';
import { DynamicComponentModel } from '../../services/dynamic-component-mapper.service';
import { CardViewBaseItemModel } from './card-view-baseitem.model';
import { CardViewTextItemPipeProperty, CardViewTextItemProperties } from '../interfaces/card-view.interfaces';
import { PipeTransform } from '@angular/core';
import { CardViewItem } from '../interface/card-view-item.interface';
import { DynamicComponentModel } from '../services/dynamic-component-mapper.service';
import { CardViewBaseItemModel, CardViewItemProperties } from './card-view-baseitem.model';
export interface CardViewTextItemPipeProperty {
pipe: PipeTransform;
params?: Array<any>;
}
export interface CardViewTextItemProperties extends CardViewItemProperties {
multiline?: boolean;
pipes?: Array<CardViewTextItemPipeProperty>;
}
export class CardViewTextItemModel extends CardViewBaseItemModel implements CardViewItem, DynamicComponentModel {
type: string = 'text';
multiline?: boolean;
pipes?: Array<CardViewTextItemPipeProperty>;
pipes?: CardViewTextItemPipeProperty[];
constructor(obj: CardViewTextItemProperties) {
super(obj);
@ -48,8 +32,12 @@ export class CardViewTextItemModel extends CardViewBaseItemModel implements Card
}
get displayValue() {
if (this.isEmpty()) {
return this.default;
} else {
return this.applyPipes(this.value);
}
}
private applyPipes(displayValue) {
if (this.pipes.length) {

View File

@ -0,0 +1,25 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './card-view-baseitem.model';
export * from './card-view-boolitem.model';
export * from './card-view-dateitem.model';
export * from './card-view-datetimeitem.model';
export * from './card-view-floatitem.model';
export * from './card-view-intitem.model';
export * from './card-view-mapitem.model';
export * from './card-view-textitem.model';

View File

@ -15,9 +15,16 @@
* limitations under the License.
*/
export * from './card-view-content-proxy.directive';
export * from './card-view-dateitem.component';
export * from './card-view-item-dispatcher.component';
export * from './card-view-mapitem.component';
export * from './card-view-textitem.component';
export * from './card-view.component';
export {
CardViewComponent,
CardViewBoolItemComponent,
CardViewDateItemComponent,
CardViewMapItemComponent,
CardViewTextItemComponent
} from './components/card-view.components';
export * from './interfaces/card-view.interfaces';
export * from './validators/card-view.validators';
export * from './models/card-view.models';
export * from './services/card-view.services';
export * from './directives/card-view-content-proxy.directive';

View File

@ -16,10 +16,11 @@
*/
import { Injectable, Type } from '@angular/core';
import { CardViewDateItemComponent } from '../card-view/card-view-dateitem.component';
import { CardViewMapItemComponent } from '../card-view/card-view-mapitem.component';
import { CardViewTextItemComponent } from '../card-view/card-view-textitem.component';
import { DynamicComponentMapper, DynamicComponentResolveFunction, DynamicComponentResolver } from '../services/dynamic-component-mapper.service';
import { CardViewDateItemComponent } from '../components/card-view-dateitem/card-view-dateitem.component';
import { CardViewMapItemComponent } from '../components/card-view-mapitem/card-view-mapitem.component';
import { CardViewTextItemComponent } from '../components/card-view-textitem/card-view-textitem.component';
import { CardViewBoolItemComponent } from '../components/card-view-boolitem/card-view-boolitem.component';
import { DynamicComponentMapper, DynamicComponentResolveFunction, DynamicComponentResolver } from '../../services/dynamic-component-mapper.service';
@Injectable()
export class CardItemTypeService extends DynamicComponentMapper {
@ -28,7 +29,11 @@ export class CardItemTypeService extends DynamicComponentMapper {
protected types: { [key: string]: DynamicComponentResolveFunction } = {
'text': DynamicComponentResolver.fromType(CardViewTextItemComponent),
'int': DynamicComponentResolver.fromType(CardViewTextItemComponent),
'float': DynamicComponentResolver.fromType(CardViewTextItemComponent),
'date': DynamicComponentResolver.fromType(CardViewDateItemComponent),
'datetime': DynamicComponentResolver.fromType(CardViewDateItemComponent),
'bool': DynamicComponentResolver.fromType(CardViewBoolItemComponent),
'map': DynamicComponentResolver.fromType(CardViewMapItemComponent)
};
}

View File

@ -0,0 +1,19 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './card-item-types.service';
export * from './card-view-update.service';

View File

@ -0,0 +1,27 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* 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 { CardViewItemValidator } from '../interfaces/card-view.interfaces';
export class CardViewItemFloatValidator implements CardViewItemValidator {
message = 'CORE.CARDVIEW.VALIDATORS.FLOAT_VALIDATION_ERROR';
isValid(value: any): boolean {
return !isNaN(parseFloat(value)) && isFinite(value);
}
}

View File

@ -0,0 +1,27 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* 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 { CardViewItemValidator } from '../interfaces/card-view.interfaces';
export class CardViewItemIntValidator implements CardViewItemValidator {
message = 'CORE.CARDVIEW.VALIDATORS.INT_VALIDATION_ERROR';
isValid(value: any): boolean {
return !isNaN(value) && (function(x) { return (x | 0) === x; })(parseFloat(value));
}
}

View File

@ -0,0 +1,19 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './card-view-item-int.validator';
export * from './card-view-item-float.validator';

View File

@ -83,8 +83,15 @@
"APPLY": "APPLY",
"NOT_VALID": "http(s)://host|ip:port(/path) not recognized, try a different URL."
},
"CARDVIEW": {
"VALIDATORS": {
"FLOAT_VALIDATION_ERROR": "The value has to be number",
"INT_VALIDATION_ERROR": "The value has to be integer"
}
},
"METADATA": {
"BASIC": {
"HEADER": "Properties",
"NAME": "Name",
"TITLE": "Title",
"DESCRIPTION": "Description",

View File

@ -16,5 +16,4 @@
*/
export * from './authentication.interface';
export * from './card-view-item.interface';
export * from './injection.tokens';

View File

@ -15,10 +15,6 @@
* limitations under the License.
*/
export * from './card-view-baseitem.model';
export * from './card-view-textitem.model';
export * from './card-view-mapitem.model';
export * from './card-view-dateitem.model';
export * from './file.model';
export * from './permissions.enum';
export * from './product-version.model';

View File

@ -19,7 +19,7 @@ import { Injectable } from '@angular/core';
import {
AlfrescoApi, ContentApi, FavoritesApi, NodesApi,
PeopleApi, RenditionsApi, SharedlinksApi, SitesApi,
VersionsApi
VersionsApi, ClassesApi
} from 'alfresco-js-api';
import * as alfrescoApi from 'alfresco-js-api';
import { AppConfigService } from '../app-config/app-config.service';
@ -70,6 +70,10 @@ export class AlfrescoApiService {
return this.getInstance().core.versionsApi;
}
get classesApi(): ClassesApi {
return this.getInstance().core.classesApi;
}
constructor(private appConfig: AppConfigService,
private storage: StorageService) {

Some files were not shown because too many files have changed in this diff Show More