[ADF-112] Change task details feature (due date) (#2071)

* Add Assignee to readonly mode

* Style adoption and first steps to editable mode

* Switch between mode coverage

* Rebase fix

* Because of design and requirement changes, revert unnecessary parts

* Small refactoring before the introduction of CardViewDateItem

* Fix AdfCardView tests

* Editable Card date item

* Do not allow edit on task details after the task is completed.

* Update task details request

* Login footer switch fix

* Login customisable copyright text

* Card text item (first sketches)

* Small fix for supported card items' template

* Dynamic component loading for card view items

* Test and linting fixes

* Updating Readme.md

* Update Readme.md

* Fix Readme.md errors

* CardViewTextItemComponent tests

* Rebase fix
This commit is contained in:
Popovics András
2017-07-13 15:49:21 +01:00
committed by Eugenio Romano
parent 08766eee23
commit 07b0e98b20
47 changed files with 1571 additions and 151 deletions

View File

@@ -29,7 +29,16 @@
* [Events](#events-1)
- [ADF Card View](#adf-card-view)
* [Properties](#properties-2)
* [CardViewModel](#cardviewmodel)
* [Editing](#editing)
* [Defining properties](#defining-properties)
* [Card Text Item](#card-text-item)
+ [Options](#options)
* [Card Date Item](#card-date-item)
+ [Options](#options-1)
* [Defining your custom card Item](#defining-your-custom-card-item)
+ [1. Define the model for the custom type](#1-define-the-model-for-the-custom-type)
+ [2. Define the component for the custom type](#2-define-the-component-for-the-custom-type)
+ [3. Add you custom component to your module's entryComponents list](#3-add-you-custom-component-to-your-modules-entrycomponents-list)
- [AlfrescoTranslationService](#alfrescotranslationservice)
- [Renditions Service](#renditions-service)
- [Build from sources](#build-from-sources)
@@ -593,40 +602,193 @@ export class AppComponent {
## ADF Card View
The component shows the [CardViewModel](#cardviewmodel)} object.
The CardViewComponent is a configurable property list renderer. You define the property list, the CardViewComponent does the rest. Each property represents a card view item (a row) in the card view component. At the time of writing two different kind of card view item (property type) is supported out of the box ([text](#card-text-item) item and [date](#card-date-item) item) but you can define your own custom types as well.
```html
<adf-card-view
[properties]="[{label: 'My Label', value: 'My value'}]">
[properties]="[{label: 'My Label', value: 'My value'}]"
[editable]="false">
</adf-card-view>
```
![adf-custom-view](docs/assets/adf-custom-view.png)
### Properties
| Name | Type | Description |
| --- | --- | --- |
| properties | {array[CardViewModel](#cardviewmodel)} | (**required**) The custom view to render |
| properties | [CardViewItem](#cardviewitem)[] | (**required**) The custom view to render |
| editable | boolean | If the component editable or not |
### CardViewModel
### Editing
```json
{
"label": "string",
"value": "any",
"format": "string",
"default": "string"
The card view can optionally allow its properties to be edited. You can control the editing of the properties in two level.
- **global level** - *via the editable paramter of the adf-card-view component*
- **property level** - *in each property via the editable attribute*
If you set the global editable parameter to false, no properties can be edited regardless of what is set inside the property.
### Defining properties
Properties is an array of models which one by one implements the CardViewItem interface.
```js
export interface CardViewItem {
label: string;
value: any;
key: string;
default?: any;
type: string;
displayValue: string;
editable?: boolean;
}
```
| Name | Type | Description |
| --- | --- | --- |
| label | string | The label to render |
| value | string | The value to render |
| format | string | The format to use in case the value is a date |
| default | string | The default value to render in case the value is empty |
At the moment two models are defined out of the box:
![adf-custom-view](docs/assets/adf-custom-view.png)
- **[CardViewTextItemModel](#card-text-item)** - *for text items*
- **[CardViewDateItemModel](#card-date-item)** - *for date items*
Each of them are extending the abstract CardViewBaseItemModel class, and each of them are adding some custom functionality to the basic behaviour.
```js
this.properties = [
new CardViewTextItemModel({
label: 'Name',
value: 'Spock',
key: 'name',
default: 'default bar' ,
multiline: false
}),
new CardViewDateItemModel({
label: 'Birth of date',
value: someDate,
key: 'birth-of-date',
default: new Date(),
format: '<any format that momentjs accepts>',
editable: true
}),
...
]
```
### Card Text Item
CardViewTextItemModel is a property type for text properties.
```js
const textItemProperty = new CardViewTextItemModel(options);
```
#### Options
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| 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. |
| default | 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 |
| multiline | string | false | Single or multiline text |
### Card Date Item
CardViewDateItemModel is a property type for date properties.
```js
const dateItemProperty = new CardViewDateItemModel(options);
```
#### Options
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| 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. |
| default | 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" | any format that momentjs accepts |
### 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.
Let's consider you want to have a **stardate** type to display Captain Picard's birthday (47457.1). For this, you need to do the following steps.
#### 1. Define the model for the custom type
Your model has to extend the CardViewBaseItemModel and implement the CardViewItem interface.
*(You can check how the CardViewTextItemModel is implemented for further guidance.)*
```js
export class CardViewStarDateItemModel extends CardViewBaseItemModel implements CardViewItem {
type: string = 'star-date';
get displayValue() {
return this.convertToStarDate(this.value) || this.default;
}
private convertToStarDate(starTimeStamp: number): string {
// Do the magic
}
}
```
The most important part of this model is the value of the **type** attribute. This is how the Card View component will be able to recognise which component is needed to render it dynamically.
The type is a **hyphen-separated-lowercase-words** string (just like how I wrote it). This will be converted to a PascalCase (or UpperCamelCase) string to find the right component. In our case the Card View component will look for the CardView**StarDate**ItemComponent.
#### 2. Define the component for the custom type
As discussed in the previous step the only important thing here is the naming of your component class ( **CardViewStarDateItemComponent**). Since the selector is not used in this case, you can give any selector name to it, but it make sense to follow the angular standards.
```js
@Component({
selector: 'card-view-stardateitem' // For example
...
})
export class CardViewStarDateItemComponent {
@Input()
property: CardViewStarDateItemModel;
@Input()
editable: boolean;
constructor(private cardViewUpdateService: CardViewUpdateService) {}
isEditble() {
return this.editable && this.property.editable;
}
showStarDatePicker() {
...
}
}
```
To make your component editable, you can have a look on either the CardViewTextItemComponent' or on the CardViewDateItemComponent's source.
#### 3. Add you custom component to your module's entryComponents list
For Angular to be able to load your custom component dynamically, you have to register your component in your modules entryComponents.
```js
@NgModule({
imports: [...],
declarations: [
CardViewStarDateItemComponent
],
entryComponents: [
CardViewStarDateItemComponent
],
exports: [...]
})
export class MyModule {}
```
## AlfrescoTranslationService

View File

@@ -60,8 +60,17 @@ module.exports = {
exclude: [/node_modules/, /bundles/, /dist/, /demo/]
},
{
test: /\.component.scss$/,
use: ['to-string-loader', 'raw-loader', 'sass-loader'],
test: /\.scss$/,
use: [{
loader: "to-string-loader"
}, {
loader: "raw-loader"
}, {
loader: "sass-loader",
options: {
includePaths: [ path.resolve(__dirname, '../styles')]
}
}],
exclude: [/node_modules/, /bundles/, /dist/, /demo/]
},
{

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 74 KiB

View File

@@ -69,6 +69,7 @@ export { ContextMenuModule } from './src/components/context-menu/context-menu.mo
export { CardViewModule } from './src/components/view/card-view.module';
export { CollapsableModule } from './src/components/collapsable/collapsable.module';
export { UserPreferencesService } from './src/services/user-preferences.service';
export { CardViewItem } from './src/interface/card-view-item.interface';
export * from './src/services/index';
export * from './src/components/data-column/data-column.component';
export * from './src/components/data-column/data-column-list.component';

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 { Directive, ViewContainerRef } from '@angular/core';
@Directive({
selector: '[adf-card-view-content-proxy]'
})
export class AdfCardViewContentProxyDirective {
constructor(public viewContainerRef: ViewContainerRef) { }
}

View File

@@ -0,0 +1,23 @@
<div class="adf-property-label">{{ property.label }}</div>
<div class="adf-property-value">
<span *ngIf="!isEditble()">
<span [attr.data-automation-id]="'card-dateitem-' + property.key">{{ property.displayValue }}</span>
</span>
<span *ngIf="isEditble()" class="adf-dateitem-editable">
<input class="adf-invisible-date-input" [mdDatepicker]="picker" value="{{property.value}}"><!--
--><span
class="adf-datepicker-toggle"
[attr.data-automation-id]="'datepicker-label-toggle-' + property.key"
(click)="showDatePicker($event)">{{ property.displayValue }}
</span>
<button
[attr.data-automation-id]="'datepickertoggle-' + property.key"
[mdDatepickerToggle]="picker">
</button>
<md-datepicker #picker
[attr.data-automation-id]="'datepicker-' + property.key"
(selectedChanged)="dateChanged($event)"
[startAt]="property.value">
</md-datepicker>
</span>
</div>

View File

@@ -0,0 +1,28 @@
@import 'theming';
.#{$ADF} {
&-invisible-date-input {
height: 24px;
width: 0;
overflow: hidden;
opacity: 0;
border: none;
margin: 0;
padding: 0;
}
&-dateitem-editable {
cursor: pointer;
button {
width: 16px;
height: 16px;
opacity: 0.5;
margin-left: 4px;
}
&:hover button {
opacity: 1;
}
}
}

View File

@@ -0,0 +1,151 @@
/*!
* @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, ComponentFixture, TestBed } from '@angular/core/testing';
import { MdDatepickerModule, MdInputModule, MdNativeDateModule } from '@angular/material';
import { By } from '@angular/platform-browser';
import { CardViewDateItemModel } from '../../models/card-view-dateitem.model';
import { CardViewUpdateService } from '../../services/adf-card-view-update.service';
import { CardViewDateItemComponent } from './adf-card-view-dateitem.component';
describe('CardViewDateItemComponent', () => {
let fixture: ComponentFixture<CardViewDateItemComponent>;
let component: CardViewDateItemComponent;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
MdDatepickerModule,
MdInputModule,
MdNativeDateModule
],
declarations: [
CardViewDateItemComponent
],
providers: [
CardViewUpdateService
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CardViewDateItemComponent);
component = fixture.componentInstance;
component.property = <CardViewDateItemModel>{
type: 'date',
label: 'Date label',
value: new Date('07/10/2017'),
key: 'datekey',
default: '',
format: '',
editable: false,
get displayValue(): string {
return 'Jul 10 2017';
}
};
});
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('Date label');
let value = fixture.debugElement.query(By.css('.adf-property-value'));
expect(value).not.toBeNull();
expect(value.nativeElement.innerText.trim()).toBe('Jul 10 2017');
});
it('should render value when editable:true', () => {
component.editable = true;
component.property.editable = true;
fixture.detectChanges();
let value = fixture.debugElement.query(By.css('.adf-property-value'));
expect(value).not.toBeNull();
expect(value.nativeElement.innerText.trim()).toBe('Jul 10 2017');
});
it('should render the picker and toggle in case of editable:true', () => {
component.editable = true;
component.property.editable = true;
fixture.detectChanges();
let datePicker = fixture.debugElement.query(By.css(`[data-automation-id="datepicker-${component.property.key}"]`));
let datePickerToggle = fixture.debugElement.query(By.css(`[data-automation-id="datepickertoggle-${component.property.key}"]`));
expect(datePicker).not.toBeNull('Datepicker should be in DOM');
expect(datePickerToggle).not.toBeNull('Datepicker toggle should be shown');
});
it('should NOT render the picker and toggle in case of editable:false', () => {
component.property.editable = false;
fixture.detectChanges();
let datePicker = fixture.debugElement.query(By.css(`[data-automation-id="datepicker-${component.property.key}"]`));
let datePickerToggle = fixture.debugElement.query(By.css(`[data-automation-id="datepickertoggle-${component.property.key}"]`));
expect(datePicker).toBeNull('Datepicker should NOT be in DOM');
expect(datePickerToggle).toBeNull('Datepicker toggle 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 datePicker = fixture.debugElement.query(By.css(`[data-automation-id="datepicker-${component.property.key}"]`));
let datePickerToggle = fixture.debugElement.query(By.css(`[data-automation-id="datepickertoggle-${component.property.key}"]`));
expect(datePicker).toBeNull('Datepicker should NOT be in DOM');
expect(datePickerToggle).toBeNull('Datepicker toggle should NOT be shown');
});
it('should open the datetimepicker when clicking on the label', () => {
component.editable = true;
component.property.editable = true;
fixture.detectChanges();
spyOn(component.datepicker, 'open');
let datePickerLabelToggle = fixture.debugElement.query(By.css(`[data-automation-id="datepicker-label-toggle-${component.property.key}"]`));
datePickerLabelToggle.triggerEventHandler('click', {});
expect(component.datepicker.open).toHaveBeenCalled();
});
it('should trigger an update event on the CardViewUpdateService', (done) => {
component.editable = true;
component.property.editable = true;
const cardViewUpdateService = TestBed.get(CardViewUpdateService);
const expectedDate = new Date('11/11/2017');
fixture.detectChanges();
cardViewUpdateService.itemUpdated$.subscribe(
(updateNotification) => {
expect(updateNotification.target).toBe(component.property);
expect(updateNotification.changed).toEqual({ datekey: expectedDate });
done();
}
);
component.datepicker.selectedChanged.next(expectedDate);
});
});

View File

@@ -0,0 +1,51 @@
/*!
* @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, ViewChild } from '@angular/core';
import { MdDatepicker } from '@angular/material';
import { CardViewDateItemModel } from '../../models/card-view-dateitem.model';
import { CardViewUpdateService } from '../../services/adf-card-view-update.service';
@Component({
selector: 'adf-card-view-dateitem',
templateUrl: './adf-card-view-dateitem.component.html',
styleUrls: ['./adf-card-view-dateitem.component.scss']
})
export class CardViewDateItemComponent {
@Input()
property: CardViewDateItemModel;
@Input()
editable: boolean;
@ViewChild(MdDatepicker)
public datepicker: MdDatepicker<any>;
constructor(private cardViewUpdateService: CardViewUpdateService) {}
isEditble() {
return this.editable && this.property.editable;
}
showDatePicker() {
this.datepicker.open();
}
dateChanged(changed) {
this.cardViewUpdateService.update(this.property, { [this.property.key]: changed });
}
}

View File

@@ -0,0 +1,155 @@
/*!
* @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 { 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 { AdfCardViewContentProxyDirective } from './adf-card-view-content-proxy.directive';
import { CardViewItemDispatcherComponent } from './adf-card-view-item-dispatcher.component';
@Component({
selector: 'whatever-you-want-to-have',
template: '<div data-automation-id="found-me">Hey I am shiny!</div>'
})
export class CardViewShinyCustomElementItemComponent {
@Input() property: CardViewItem;
@Input() editable: boolean;
}
describe('CardViewItemDispatcherComponent', () => {
let fixture: ComponentFixture<CardViewItemDispatcherComponent>;
let component: CardViewItemDispatcherComponent;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [],
declarations: [
CardViewItemDispatcherComponent,
CardViewShinyCustomElementItemComponent,
AdfCardViewContentProxyDirective
],
providers: []
});
// entryComponents are not supported yet on TestBed, that is why this ugly workaround:
// https://github.com/angular/angular/issues/10760
TestBed.overrideModule(BrowserDynamicTestingModule, {
set: { entryComponents: [ CardViewShinyCustomElementItemComponent ] }
});
TestBed.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CardViewItemDispatcherComponent);
component = fixture.componentInstance;
component.property = <CardViewItem>{
type: 'shiny-custom-element',
label: 'Shiny custom element',
value: null,
key: 'customkey',
default: '',
editable: false,
get displayValue(): string {
return 'custom value displayed';
}
};
component.editable = true;
fixture.detectChanges();
component.ngOnChanges(null);
});
afterEach(() => {
fixture.destroy();
TestBed.resetTestingModule();
});
describe('Subcompomnent creation', () => {
it('should load the CardViewShinyCustomElementItemComponent', () => {
const innerElement = fixture.debugElement.query(By.css('[data-automation-id="found-me"]'));
expect(innerElement).not.toBeNull();
});
it('should load the CardViewShinyCustomElementItemComponent only ONCE', () => {
component.ngOnChanges();
component.ngOnChanges();
component.ngOnChanges();
fixture.detectChanges();
const shinyCustomElementItemComponent = fixture.debugElement.queryAll(By.css('whatever-you-want-to-have'));
expect(shinyCustomElementItemComponent.length).toEqual(1);
});
it('should pass through the property and editable 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);
});
});
describe('Angular lifecycle methods', () => {
let shinyCustomElementItemComponent;
const lifeCycleMethods = [
'ngOnChanges',
'ngOnInit',
'ngDoCheck',
'ngAfterContentInit',
'ngAfterContentChecked',
'ngAfterViewInit',
'ngAfterViewChecked',
'ngOnDestroy'
];
beforeEach(() => {
shinyCustomElementItemComponent = fixture.debugElement.query(By.css('whatever-you-want-to-have')).componentInstance;
});
it('should call through the lifecycle methods', () => {
lifeCycleMethods.forEach((lifeCycleMethod) => {
shinyCustomElementItemComponent[lifeCycleMethod] = () => {};
spyOn(shinyCustomElementItemComponent, lifeCycleMethod);
const param1 = {};
const param2 = {};
component[lifeCycleMethod].call(component, param1, param2);
expect(shinyCustomElementItemComponent[lifeCycleMethod]).toHaveBeenCalledWith(param1, param2);
});
});
it('should NOT call through the lifecycle methods if the method does not exist (no error should be thrown)', () => {
lifeCycleMethods.forEach((lifeCycleMethod) => {
shinyCustomElementItemComponent[lifeCycleMethod] = undefined;
const execution = () => {
component[lifeCycleMethod].call(component);
};
expect(execution).not.toThrowError();
});
});
});
});

View File

@@ -0,0 +1,97 @@
/*!
* @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,
ComponentFactoryResolver,
Input,
OnChanges,
Type,
ViewChild
} from '@angular/core';
import { CardViewItem } from '../../interface/card-view-item.interface';
import { AdfCardViewContentProxyDirective } from './adf-card-view-content-proxy.directive';
@Component({
selector: 'adf-card-view-item-dispatcher',
template: '<ng-template adf-card-view-content-proxy></ng-template>'
})
export class CardViewItemDispatcherComponent implements OnChanges {
@Input()
property: CardViewItem;
@Input()
editable: boolean;
@ViewChild(AdfCardViewContentProxyDirective)
private content: AdfCardViewContentProxyDirective;
private loaded: boolean = false;
private componentReference: any = null;
public ngOnInit;
public ngDoCheck;
constructor(private resolver: ComponentFactoryResolver) {
const dynamicLifecycleMethods = [
'ngOnInit',
'ngDoCheck',
'ngAfterContentInit',
'ngAfterContentChecked',
'ngAfterViewInit',
'ngAfterViewChecked',
'ngOnDestroy'
];
dynamicLifecycleMethods.forEach((dynamicLifecycleMethod) => {
this[dynamicLifecycleMethod] = this.proxy.bind(this, dynamicLifecycleMethod);
});
}
ngOnChanges(...args) {
if (!this.loaded) {
this.loadComponent();
this.loaded = true;
}
this.proxy('ngOnChanges', ...args);
}
private loadComponent() {
const upperCamelCasedType = this.getUpperCamelCase(this.property.type),
className = `CardView${upperCamelCasedType}ItemComponent`;
const factories = Array.from(this.resolver['_factories'].keys());
const factoryClass = <Type<any>>factories.find((x: any) => x.name === className);
const factory = this.resolver.resolveComponentFactory(factoryClass);
this.componentReference = this.content.viewContainerRef.createComponent(factory);
this.componentReference.instance.editable = this.editable;
this.componentReference.instance.property = this.property;
}
private getUpperCamelCase(type: string): string {
const camelCasedType = type.replace(/-([a-z])/g, function (g) { return g[1].toUpperCase(); });
return camelCasedType[0].toUpperCase() + camelCasedType.substr(1);
}
private proxy(methodName, ...args) {
if (this.componentReference.instance[methodName]) {
this.componentReference.instance[methodName].apply(this.componentReference.instance, args);
}
}
}

View File

@@ -0,0 +1,39 @@
<div class="adf-property-label">{{ property.label }}</div>
<div class="adf-property-value">
<span *ngIf="!isEditble()">
<span [attr.data-automation-id]="'card-textitem-value-' + property.key">{{ property.displayValue }}</span>
</span>
<span *ngIf="isEditble()">
<div *ngIf="!inEdit" (click)="setEditMode(true)" class="adf-textitem-readonly" [attr.data-automation-id]="'card-textitem-edit-toggle-' + property.key">
<span [attr.data-automation-id]="'card-textitem-value-' + property.key">{{ property.displayValue }}</span>
<md-icon [attr.data-automation-id]="'card-textitem-edit-icon-' + property.key" class="adf-textitem-icon">create</md-icon>
</div>
<div *ngIf="inEdit" class="adf-textitem-editable">
<md-input-container floatPlaceholder="never" class="adf-input-container">
<input *ngIf="!property.multiline" #editorInput
mdInput
class="adf-input"
[placeholder]="property.default"
[(ngModel)]="editedValue"
[attr.data-automation-id]="'card-textitem-editinput-' + property.key">
<textarea *ngIf="property.multiline" #editorInput
mdInput
mdTextareaAutosize
mdAutosizeMaxRows="1"
mdAutosizeMaxRows="5"
class="adf-textarea"
[placeholder]="property.default"
[(ngModel)]="editedValue"
[attr.data-automation-id]="'card-textitem-edittextarea-' + property.key"></textarea>
</md-input-container>
<md-icon
class="adf-textitem-icon adf-update-icon"
(click)="update()"
[attr.data-automation-id]="'card-textitem-update-' + property.key">done</md-icon>
<md-icon
class="adf-textitem-icon adf-reset-icon"
(click)="reset()"
[attr.data-automation-id]="'card-textitem-reset-' + property.key">clear</md-icon>
</div>
</span>
</div>

View File

@@ -0,0 +1,80 @@
@import 'theming';
.#{$ADF} {
&-textitem-icon {
font-size: 16px;
width: 16px;
height: 16px;
position: relative;
top: 3px;
padding-left: 8px;
opacity: 0.5;
}
&-update-icon {
padding-left: 13px;
}
&-textitem-readonly {
cursor: pointer;
&:hover md-icon {
opacity: 1;
}
}
&-textitem-editable {
display: flex;
md-icon:hover {
opacity: 1;
cursor: pointer;
}
md-input-container {
width: 100%;
}
input:focus,
textarea:focus {
border: 1px solid #EEE;
}
}
&-textitem-editable /deep/ .mat-input-wrapper {
margin: 0;
padding-bottom: 0;
}
&-textitem-editable /deep/ .mat-input-underline {
display: none;
}
&-textitem-editable /deep/ .mat-input-placeholder-wrapper {
padding-top: 2em;
}
&-textitem-editable /deep/ .mat-input-placeholder {
top: 3px;
}
&-textitem-editable /deep/ .mat-input-element {
font-family: inherit;
position: relative;
padding-top: 3px;
}
&-textitem-editable /deep/ .mat-input-element:focus {
padding: 5px;
left: -6px;
top: -3px;
}
&-textitem-editable /deep/ input.mat-input-element {
margin-bottom: 2px;
}
&-textitem-editable /deep/ input.mat-input-element:focus {
margin-bottom: -7px;
}
}

View File

@@ -0,0 +1,147 @@
/*!
* @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, ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { MdDatepickerModule, MdIconModule, MdInputModule, MdNativeDateModule } from '@angular/material';
import { By } from '@angular/platform-browser';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { CardViewTextItemModel } from '../../models/card-view-textitem.model';
import { CardViewUpdateService } from '../../services/adf-card-view-update.service';
import { CardViewTextItemComponent } from './adf-card-view-textitem.component';
describe('CardViewTextItemComponent', () => {
let fixture: ComponentFixture<CardViewTextItemComponent>;
let component: CardViewTextItemComponent;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
FormsModule,
NoopAnimationsModule,
MdDatepickerModule,
MdIconModule,
MdInputModule,
MdNativeDateModule
],
declarations: [
CardViewTextItemComponent
],
providers: [
CardViewUpdateService
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CardViewTextItemComponent);
component = fixture.componentInstance;
component.property = <CardViewTextItemModel>{
type: 'text',
label: 'Text label',
value: 'Lorem ipsum',
key: 'textkey',
default: '',
editable: false,
get displayValue(): string {
return 'Lorem ipsum';
}
};
});
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 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);
});
});

View File

@@ -0,0 +1,65 @@
/*!
* @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, OnChanges, ViewChild } from '@angular/core';
import { CardViewTextItemModel } from '../../models/card-view-textitem.model';
import { CardViewUpdateService } from '../../services/adf-card-view-update.service';
@Component({
selector: 'adf-card-view-textitem',
templateUrl: './adf-card-view-textitem.component.html',
styleUrls: ['./adf-card-view-textitem.component.scss']
})
export class CardViewTextItemComponent implements OnChanges {
@Input()
property: CardViewTextItemModel;
@Input()
editable: boolean;
@ViewChild('editorInput')
private editorInput: any;
inEdit: boolean = false;
editedValue: string;
constructor(private cardViewUpdateService: CardViewUpdateService) {}
ngOnChanges() {
this.editedValue = this.property.value;
}
isEditble() {
return this.editable && this.property.editable;
}
setEditMode(editStatus: boolean): void {
this.inEdit = editStatus;
setTimeout(() => {
this.editorInput.nativeElement.click();
}, 0);
}
reset(): void {
this.editedValue = this.property.value;
this.setEditMode(false);
}
update(): void {
this.cardViewUpdateService.update(this.property, { [this.property.key]: this.editedValue });
}
}

View File

@@ -1,18 +0,0 @@
:host {
width: 100%;
}
.adf-header__label {
font-weight: bold;
float: left;
width: 50%;
word-wrap: break-word;
}
.adf-header__value {
color: rgb(68, 138, 255);
}
.material-icons {
cursor: pointer;
}

View File

@@ -1,6 +1,7 @@
<div class="mdl-cell mdl-cell--12-col" *ngFor="let property of properties; let idx = index">
<div [attr.data-automation-id]="'header-' + property.key">
<div class="adf-header__label">{{ property.label }}</div>
<div class="adf-header__value">{{ getPropertyValue(property) }}</div>
</div>
<div class="adf-property-list">
<ng-container *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>
</ng-container>
</div>

View File

@@ -0,0 +1,32 @@
@import 'theming';
.#{$ADF} {
&-property-list {
display: table;
width: 100%;
border-collapse: collapse;
border-spacing: 0;
}
&-property {
display: table-row;
}
&-property /deep/ &-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 /deep/ &-property-value {
width: 100%;
display: table-cell;
color: rgb(101, 101, 101);
vertical-align: top;
padding-bottom: 20px;
}
}

View File

@@ -16,8 +16,17 @@
*/
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { MdDatepickerModule, MdIconModule, MdInputModule, MdNativeDateModule } from '@angular/material';
import { By } from '@angular/platform-browser';
import { CardViewModel } from '../../models/card-view.model';
import { BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing';
import { CardViewDateItemModel } from '../../models/card-view-dateitem.model';
import { CardViewTextItemModel } from '../../models/card-view-textitem.model';
import { CardViewUpdateService } from '../../services/adf-card-view-update.service';
import { AdfCardViewContentProxyDirective } from './adf-card-view-content-proxy.directive';
import { CardViewDateItemComponent } from './adf-card-view-dateitem.component';
import { CardViewItemDispatcherComponent } from './adf-card-view-item-dispatcher.component';
import { CardViewTextItemComponent } from './adf-card-view-textitem.component';
import { CardViewComponent } from './adf-card-view.component';
describe('AdfCardView', () => {
@@ -27,12 +36,32 @@ describe('AdfCardView', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
MdDatepickerModule,
MdIconModule,
MdInputModule,
MdNativeDateModule,
FormsModule
],
declarations: [
CardViewComponent
CardViewComponent,
CardViewItemDispatcherComponent,
AdfCardViewContentProxyDirective,
CardViewTextItemComponent,
CardViewDateItemComponent
],
providers: [
CardViewUpdateService
]
}).compileComponents();
});
// entryComponents are not supported yet on TestBed, that is why this ugly workaround:
// https://github.com/angular/angular/issues/10760
TestBed.overrideModule(BrowserDynamicTestingModule, {
set: { entryComponents: [ CardViewTextItemComponent, CardViewDateItemComponent ] }
});
TestBed.compileComponents();
}));
beforeEach(() => {
@@ -41,59 +70,73 @@ describe('AdfCardView', () => {
});
it('should render the label and value', async(() => {
component.properties = [new CardViewModel({label: 'My label', value: 'My value'})];
component.properties = [new CardViewTextItemModel({label: 'My label', value: 'My value', key: 'some key'})];
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
let labelValue = fixture.debugElement.query(By.css('.adf-header__label'));
let labelValue = fixture.debugElement.query(By.css('.adf-property-label'));
expect(labelValue).not.toBeNull();
expect(labelValue.nativeElement.innerText).toBe('My label');
let value = fixture.debugElement.query(By.css('.adf-header__value'));
let value = fixture.debugElement.query(By.css('.adf-property-value'));
expect(value).not.toBeNull();
expect(value.nativeElement.innerText).toBe('My value');
});
}));
it('should pass through editable property to the items', () => {
component.editable = true;
component.properties = [new CardViewDateItemModel({
label: 'My date label',
value: '2017-06-14',
key: 'some-key',
editable: true
})];
fixture.detectChanges();
let datePicker = fixture.debugElement.query(By.css(`[data-automation-id="datepicker-some-key"]`));
expect(datePicker).not.toBeNull('Datepicker should be in DOM');
});
it('should render the date in the correct format', async(() => {
component.properties = [new CardViewModel({
label: 'My date label', value: '2017-06-14',
component.properties = [new CardViewDateItemModel({
label: 'My date label', value: '2017-06-14', key: 'some key',
format: 'MMM DD YYYY'
})];
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
let labelValue = fixture.debugElement.query(By.css('.adf-header__label'));
let labelValue = fixture.debugElement.query(By.css('.adf-property-label'));
expect(labelValue).not.toBeNull();
expect(labelValue.nativeElement.innerText).toBe('My date label');
let value = fixture.debugElement.query(By.css('.adf-header__value'));
let value = fixture.debugElement.query(By.css('.adf-property-value'));
expect(value).not.toBeNull();
expect(value.nativeElement.innerText).toBe('Jun 14 2017');
});
}));
it('should render the default value if the value is empty', async(() => {
component.properties = [new CardViewModel({
component.properties = [new CardViewTextItemModel({
label: 'My default label',
default: 'default value'
value: null,
default: 'default value',
key: 'some key'
})];
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
let labelValue = fixture.debugElement.query(By.css('.adf-header__label'));
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-header__value'));
let value = fixture.debugElement.query(By.css('.adf-property-value'));
expect(value).not.toBeNull();
expect(value.nativeElement.innerText).toBe('default value');
});
}));
});

View File

@@ -16,26 +16,17 @@
*/
import { Component, Input } from '@angular/core';
import * as moment from 'moment';
import { CardViewModel } from '../../models/card-view.model';
import { CardViewItem } from '../../interface/card-view-item.interface';
@Component({
selector: 'adf-card-view',
templateUrl: './adf-card-view.component.html',
styleUrls: ['./adf-card-view.component.css']
styleUrls: ['./adf-card-view.component.scss']
})
export class CardViewComponent {
@Input()
properties: CardViewItem [];
@Input()
properties: CardViewModel [];
getPropertyValue(property: CardViewModel): string {
if (!property.value) {
return property.default;
} else if (property.format) {
return moment(property.value).format(property.format);
}
return property.value;
}
editable: boolean;
}

View File

@@ -17,14 +17,34 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MdButtonModule, MdDatepickerModule, MdIconModule, MdInputModule, MdNativeDateModule } from '@angular/material';
import { AdfCardViewContentProxyDirective } from './adf-card-view-content-proxy.directive';
import { CardViewDateItemComponent } from './adf-card-view-dateitem.component';
import { CardViewItemDispatcherComponent } from './adf-card-view-item-dispatcher.component';
import { CardViewTextItemComponent } from './adf-card-view-textitem.component';
import { CardViewComponent } from './adf-card-view.component';
@NgModule({
imports: [
CommonModule
CommonModule,
MdDatepickerModule,
MdNativeDateModule,
MdInputModule,
MdIconModule,
MdButtonModule,
FormsModule
],
declarations: [
CardViewComponent
CardViewComponent,
CardViewItemDispatcherComponent,
AdfCardViewContentProxyDirective,
CardViewTextItemComponent,
CardViewDateItemComponent
],
entryComponents: [
CardViewTextItemComponent,
CardViewDateItemComponent
],
exports: [
CardViewComponent

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 CardViewItem {
label: string;
value: any;
key: string;
default?: any;
type: string;
displayValue: string;
editable?: boolean;
}

View File

@@ -20,21 +20,29 @@
* This object represent the basic structure of a card view.
*
*
* @returns {CardViewModel} .
* @returns {CardViewBaseItemModel} .
*/
export class CardViewModel {
export interface CardViewItemProperties {
label: string;
value: any;
key: any;
format: string;
default: string;
default?: string;
editable?: boolean;
}
constructor(obj?: any) {
export abstract class CardViewBaseItemModel {
label: string;
value: any;
key: any;
default: string;
editable: boolean;
constructor(obj: CardViewItemProperties) {
this.label = obj.label || '';
this.value = obj.value;
this.key = obj.key;
this.format = obj.format;
this.default = obj.default;
this.editable = obj.editable || false;
}
}

View File

@@ -0,0 +1,50 @@
/*!
* @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.
*/
/**
*
* This object represent the basic structure of a card view.
*
*
* @returns {CardViewDateItemModel} .
*/
import * as moment from 'moment';
import { CardViewItem } from '../interface/card-view-item.interface';
import { CardViewBaseItemModel, CardViewItemProperties } from './card-view-baseitem.model';
export interface CardViewDateItemProperties extends CardViewItemProperties {
format?: string;
}
export class CardViewDateItemModel extends CardViewBaseItemModel implements CardViewItem {
type: string = 'date';
format: string;
constructor(obj: CardViewDateItemProperties) {
super(obj);
this.format = obj.format || 'MMM DD YYYY';
}
get displayValue() {
if (!this.value) {
return this.default;
} else {
return moment(this.value).format(this.format);
}
}
}

View File

@@ -0,0 +1,44 @@
/*!
* @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.
*/
/**
*
* This object represent the basic structure of a card view.
*
*
* @returns {CardViewTextItemModel} .
*/
import { CardViewItem } from '../interface/card-view-item.interface';
import { CardViewBaseItemModel, CardViewItemProperties } from './card-view-baseitem.model';
export interface CardViewTextItemProperties extends CardViewItemProperties {
multiline?: boolean;
}
export class CardViewTextItemModel extends CardViewBaseItemModel implements CardViewItem {
type: string = 'text';
multiline: boolean;
constructor(obj: CardViewTextItemProperties) {
super(obj);
this.multiline = obj.multiline || false;
}
get displayValue() {
return this.value || this.default;
}
}

View File

@@ -15,5 +15,6 @@
* limitations under the License.
*/
export * from './card-view.model';
export * from './card-view-textitem.model';
export * from './card-view-dateitem.model';
export * from './file.model';

View File

@@ -0,0 +1,42 @@
/*!
* @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 { Subject } from 'rxjs/Subject';
import { CardViewBaseItemModel } from '../models/card-view-baseitem.model';
export interface UpdateNotification {
target: any;
changed: any;
}
@Injectable()
export class CardViewUpdateService {
// Observable sources
private itemUpdatedSource = new Subject<UpdateNotification>();
// Observable streams
public itemUpdated$ = this.itemUpdatedSource.asObservable();
update(property: CardViewBaseItemModel, changed: any) {
this.itemUpdatedSource.next({
target: property,
changed
});
}
}

View File

@@ -33,3 +33,4 @@ export * from './alfresco-translate-loader.service';
export * from './app-config.service';
export * from './thumbnail.service';
export * from './upload.service';
export * from './adf-card-view-update.service';