[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 8a1281475c
commit f6c3fafe32
47 changed files with 1571 additions and 151 deletions

View File

@@ -23,7 +23,7 @@
</p>
<p class="toggle">
<label for="switch4" class="mdl-switch mdl-js-switch mdl-js-ripple-effect">
<input type="checkbox" id="switch4" class="mdl-switch__input" [checked]="showFooter" (click)="showFooter = !showFooter">
<input type="checkbox" id="switch4" class="mdl-switch__input" [checked]="showFooter" (click)="toggleFooter()">
<span class="mdl-switch__label">Login footer</span>
</label>
</p>
@@ -42,6 +42,7 @@
[disableCsrf]="disableCsrf"
[showLoginActions]="showFooter"
[showRememberMe]="showFooter"
copyrightText="© 2016 Alfresco Software, Inc. All Rights Reserved. (customised text)"
(onSuccess)="onLogin($event)"
(onError)="onError($event)">
<div class="mobile-settings">
@@ -67,7 +68,7 @@
</p>
<p>
<label for="switch4-mobile" class="mdl-switch mdl-js-switch mdl-js-ripple-effect">
<input type="checkbox" id="switch4-mobile" class="mdl-switch__input" [checked]="showFooter" (click)="showFooter = !showFooter">
<input type="checkbox" id="switch4-mobile" class="mdl-switch__input" [checked]="showFooter" (click)="toggleFooter()">
<span class="mdl-switch__label">Login footer</span>
</label>
</p>

View File

@@ -95,6 +95,10 @@ export class LoginDemoComponent implements OnInit {
this.disableCsrf = !this.disableCsrf;
}
toggleFooter() {
this.showFooter = !this.showFooter;
}
updateProvider() {
if (this.isBPM && this.isECM) {
this.providers = 'ALL';

View File

@@ -65,8 +65,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, '../../ng2-alfresco-core/styles')]
}
}],
exclude: [/node_modules/, /bundles/, /dist/, /demo/]
},
{

View File

@@ -20,6 +20,7 @@ import { ModuleWithProviders, NgModule } from '@angular/core';
import {
MdAutocompleteModule,
MdButtonModule,
MdCardModule,
MdDatepickerModule,
MdGridListModule,
MdIconModule,
@@ -91,6 +92,7 @@ export const ACTIVITI_TASKLIST_PROVIDERS: any[] = [
MdIconModule,
MdButtonModule,
MdInputModule,
MdCardModule,
MdProgressSpinnerModule,
MdDatepickerModule,
MdNativeDateModule,

View File

@@ -13,7 +13,7 @@
font-weight: 300;
line-height: normal;
overflow: hidden;
margin: 0;
margin: 0 0 8px 0;
cursor: pointer;
user-select: none;
-moz-user-select: none;

View File

@@ -27,7 +27,7 @@ import { Component,
ViewChild
} from '@angular/core';
import { ContentLinkModel, FormModel, FormOutcomeEvent, FormService } from 'ng2-activiti-form';
import { AlfrescoAuthenticationService, AlfrescoTranslationService, LogService } from 'ng2-alfresco-core';
import { AlfrescoAuthenticationService, AlfrescoTranslationService, CardViewUpdateService, LogService, UpdateNotification } from 'ng2-alfresco-core';
import { TaskQueryRequestRepresentationModel } from '../models/filter.model';
import { TaskDetailsModel } from '../models/task-details.model';
import { User } from '../models/user.model';
@@ -39,7 +39,10 @@ declare let dialogPolyfill: any;
@Component({
selector: 'adf-task-details, activiti-task-details',
templateUrl: './activiti-task-details.component.html',
styleUrls: ['./activiti-task-details.component.css']
styleUrls: ['./activiti-task-details.component.css'],
providers: [
CardViewUpdateService
]
})
export class ActivitiTaskDetailsComponent implements OnInit, OnChanges {
@@ -132,17 +135,20 @@ export class ActivitiTaskDetailsComponent implements OnInit, OnChanges {
noTaskDetailsTemplateComponent: TemplateRef<any>;
/**
* Constructor
* @param auth Authentication service
* @param translate Translation service
* @param activitiForm Form service
* @param activitiTaskList Task service
*
* @param translateService
* @param activitiForm
* @param activitiTaskList
* @param logService
* @param authService
*/
constructor(private translateService: AlfrescoTranslationService,
private activitiForm: FormService,
private activitiTaskList: ActivitiTaskListService,
private logService: LogService,
private authService: AlfrescoAuthenticationService) {
private authService: AlfrescoAuthenticationService,
private cardViewUpdateService: CardViewUpdateService
) {
if (translateService) {
translateService.addTranslationFolder('ng2-activiti-tasklist', 'assets/ng2-activiti-tasklist');
@@ -153,6 +159,8 @@ export class ActivitiTaskDetailsComponent implements OnInit, OnChanges {
if (this.taskId) {
this.loadDetails(this.taskId);
}
this.cardViewUpdateService.itemUpdated$.subscribe(this.updateTaskDetails.bind(this));
}
ngOnChanges(changes: SimpleChanges): void {
@@ -186,6 +194,18 @@ export class ActivitiTaskDetailsComponent implements OnInit, OnChanges {
return this.taskDetails && this.taskDetails.duration === null;
}
/**
* Save a task detail and update it after a successful response
*
* @param updateNotification
*/
private updateTaskDetails(updateNotification: UpdateNotification) {
this.activitiTaskList.updateTask(this.taskId, updateNotification.changed)
.subscribe(
() => { this.loadDetails(this.taskId); }
);
}
/**
* Load the activiti task details
* @param taskId

View File

@@ -1,15 +0,0 @@
:host {
width: 100%;
}
.activiti-task-header__label {
font-weight: bold;
}
.activiti-task-header__value {
color: rgb(68, 138, 255);
}
.material-icons {
cursor: pointer;
}

View File

@@ -1,11 +1,13 @@
<div *ngIf="taskDetails">
<div class="mdl-grid mdl-shadow--2dp">
<md-card *ngIf="taskDetails">
<adf-card-view [properties]="properties"></adf-card-view>
<md-card-content>
<adf-card-view [properties]="properties" [editable]="!isCompleted()"></adf-card-view>
</md-card-content>
<button *ngIf="!isAssignedToMe()" data-automation-id="header-claim-button" type="button" id="claim-task"
(click)="claimTask(taskDetails.id)" class="mdl-button">{{ 'TASK_DETAILS.BUTTON.CLAIM' | translate }}
<md-card-actions class="adf-controls">
<button md-button *ngIf="!isAssignedToMe()" data-automation-id="header-claim-button" id="claim-task"
(click)="claimTask(taskDetails.id)" class="adf-claim-controls">{{ 'TASK_DETAILS.BUTTON.CLAIM' | translate }}
</button>
</md-card-actions>
</div>
</div>
</md-card>

View File

@@ -0,0 +1,24 @@
@import 'theming';
.#{$ADF} {
&-controls {
display: flex;
justify-content: space-between;
}
&-edit-controls {
display: flex;
justify-content: flex-end;
margin-left: auto;
}
&-switch-to-edit-mode,
&-save-edit-mode {
color: rgb(255, 152, 0);
}
&-cancel-edit-mode,
&-claim-controls {
color: rgb(131, 131, 131);
}
}

View File

@@ -15,9 +15,10 @@
* limitations under the License.
*/
import { DebugElement } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { AlfrescoTranslationService, CoreModule } from 'ng2-alfresco-core';
import { AlfrescoTranslationService, CardViewUpdateService, CoreModule } from 'ng2-alfresco-core';
import { Observable } from 'rxjs/Rx';
import { TaskDetailsModel } from '../models/task-details.model';
@@ -31,6 +32,7 @@ describe('ActivitiTaskHeaderComponent', () => {
let componentHandler: any;
let component: ActivitiTaskHeaderComponent;
let fixture: ComponentFixture<ActivitiTaskHeaderComponent>;
let debugElement: DebugElement;
beforeEach(async(() => {
TestBed.configureTestingModule({
@@ -41,7 +43,8 @@ describe('ActivitiTaskHeaderComponent', () => {
ActivitiTaskHeaderComponent
],
providers: [
ActivitiTaskListService
ActivitiTaskListService,
CardViewUpdateService
]
}).compileComponents();
@@ -54,6 +57,7 @@ describe('ActivitiTaskHeaderComponent', () => {
fixture = TestBed.createComponent(ActivitiTaskHeaderComponent);
component = fixture.componentInstance;
service = TestBed.get(ActivitiTaskListService);
debugElement = fixture.debugElement;
component.taskDetails = new TaskDetailsModel(taskDetailsMock);
@@ -73,7 +77,7 @@ describe('ActivitiTaskHeaderComponent', () => {
it('should display assignee', () => {
component.ngOnChanges({});
fixture.detectChanges();
let formNameEl = fixture.debugElement.query(By.css('[data-automation-id="header-assignee"] .adf-header__value'));
let formNameEl = fixture.debugElement.query(By.css('[data-automation-id="header-assignee"] .adf-property-value'));
expect(formNameEl.nativeElement.innerText).toBe('Wilbur Adams');
});
@@ -81,10 +85,43 @@ describe('ActivitiTaskHeaderComponent', () => {
component.taskDetails.assignee = null;
component.ngOnChanges({});
fixture.detectChanges();
let valueEl = fixture.debugElement.query(By.css('[data-automation-id="header-assignee"] .adf-header__value'));
let valueEl = fixture.debugElement.query(By.css('[data-automation-id="header-assignee"] .adf-property-value'));
expect(valueEl.nativeElement.innerText).toBe('No assignee');
});
it('should display created-by', () => {
component.ngOnChanges({});
fixture.detectChanges();
let formNameEl = fixture.debugElement.query(By.css('[data-automation-id="header-created-by"] .adf-property-value'));
expect(formNameEl.nativeElement.innerText).toBe('Wilbur Adams');
});
it('should display placeholder if no created-by', () => {
component.taskDetails.assignee = null;
component.ngOnChanges({});
fixture.detectChanges();
let valueEl = fixture.debugElement.query(By.css('[data-automation-id="header-created-by"] .adf-property-value'));
expect(valueEl.nativeElement.innerText).toBe('No assignee');
});
it('should set editable to false if the task has already completed', () => {
component.taskDetails.endDate = '05/05/2002';
component.ngOnChanges({});
fixture.detectChanges();
let datePicker = fixture.debugElement.query(By.css(`[data-automation-id="datepicker-dueDate"]`));
expect(datePicker).toBeNull('Datepicker should NOT be in DOM');
});
it('should set editable to true if the task has not completed yet', () => {
component.taskDetails.endDate = undefined;
component.ngOnChanges({});
fixture.detectChanges();
let datePicker = fixture.debugElement.query(By.css(`[data-automation-id="datepicker-dueDate"]`));
expect(datePicker).not.toBeNull('Datepicker should be in DOM');
});
it('should display the claim button if no assignee', () => {
component.taskDetails.assignee = null;
component.ngOnChanges({});
@@ -97,30 +134,30 @@ describe('ActivitiTaskHeaderComponent', () => {
component.taskDetails.dueDate = '2016-11-03';
component.ngOnChanges({});
fixture.detectChanges();
let valueEl = fixture.debugElement.query(By.css('[data-automation-id="header-dueDate"] .adf-header__value'));
expect(valueEl.nativeElement.innerText).toBe('Nov 03 2016');
let valueEl = fixture.debugElement.query(By.css('[data-automation-id="header-dueDate"] .adf-property-value'));
expect(valueEl.nativeElement.innerText.trim()).toBe('Nov 03 2016');
});
it('should display placeholder if no due date', () => {
component.taskDetails.dueDate = null;
component.ngOnChanges({});
fixture.detectChanges();
let valueEl = fixture.debugElement.query(By.css('[data-automation-id="header-dueDate"] .adf-header__value'));
expect(valueEl.nativeElement.innerText).toBe('No date');
let valueEl = fixture.debugElement.query(By.css('[data-automation-id="header-dueDate"] .adf-property-value'));
expect(valueEl.nativeElement.innerText.trim()).toBe('No date');
});
it('should display form name', () => {
component.formName = 'test form';
component.ngOnChanges({});
fixture.detectChanges();
let valueEl = fixture.debugElement.query(By.css('[data-automation-id="header-formName"] .adf-header__value'));
let valueEl = fixture.debugElement.query(By.css('[data-automation-id="header-formName"] .adf-property-value'));
expect(valueEl.nativeElement.innerText).toBe('test form');
});
it('should not display form name if no form name provided', () => {
component.ngOnChanges({});
fixture.detectChanges();
let valueEl = fixture.debugElement.query(By.css('[data-automation-id="header-formName"] .adf-header__value'));
let valueEl = fixture.debugElement.query(By.css('[data-automation-id="header-formName"] .adf-property-value'));
expect(valueEl.nativeElement.innerText).toBe('No form');
});

View File

@@ -16,14 +16,14 @@
*/
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import { AlfrescoTranslationService, CardViewModel, LogService } from 'ng2-alfresco-core';
import { AlfrescoTranslationService, CardViewDateItemModel, CardViewItem, CardViewTextItemModel, LogService } from 'ng2-alfresco-core';
import { TaskDetailsModel } from '../models/index';
import { ActivitiTaskListService } from './../services/activiti-tasklist.service';
@Component({
selector: 'adf-task-header, activiti-task-header',
templateUrl: './activiti-task-header.component.html',
styleUrls: ['./activiti-task-header.component.css']
styleUrls: ['./activiti-task-header.component.scss']
})
export class ActivitiTaskHeaderComponent implements OnChanges {
@@ -36,7 +36,8 @@ export class ActivitiTaskHeaderComponent implements OnChanges {
@Output()
claim: EventEmitter<any> = new EventEmitter<any>();
properties: CardViewModel [];
properties: CardViewItem [];
inEdit: boolean = false;
constructor(private translateService: AlfrescoTranslationService,
private activitiTaskService: ActivitiTaskListService,
@@ -47,6 +48,7 @@ export class ActivitiTaskHeaderComponent implements OnChanges {
}
ngOnChanges(changes: SimpleChanges) {
console.log('change van:', changes, this.taskDetails);
this.refreshData();
}
@@ -54,20 +56,22 @@ export class ActivitiTaskHeaderComponent implements OnChanges {
if (this.taskDetails) {
this.properties = [
new CardViewModel({label: 'Status:', value: this.getTaskStatus(), key: 'status'}),
new CardViewModel({label: 'Due Date:', value: this.taskDetails.dueDate, format: 'MMM DD YYYY', key: 'dueDate', default: 'No date'}),
new CardViewModel({label: 'Category:', value: this.taskDetails.category, key: 'category', default: 'No category'}),
new CardViewModel(
{
label: 'Created By:',
value: this.taskDetails.getFullName(),
key: 'assignee',
default: 'No assignee'
new CardViewTextItemModel({ label: 'Assignee', value: this.taskDetails.getFullName(), key: 'assignee', default: 'No assignee' } ),
new CardViewTextItemModel({ label: 'Status', value: this.getTaskStatus(), key: 'status' }),
new CardViewDateItemModel({ label: 'Due Date', value: this.taskDetails.dueDate, key: 'dueDate', default: 'No date', editable: true }),
new CardViewTextItemModel({ label: 'Category', value: this.taskDetails.category, key: 'category', default: 'No category' }),
new CardViewTextItemModel({ label: 'Created By', value: this.taskDetails.getFullName(), key: 'created-by', default: 'No assignee' }),
new CardViewDateItemModel({ label: 'Created', value: this.taskDetails.created, key: 'created' }),
new CardViewTextItemModel({ label: 'Id', value: this.taskDetails.id, key: 'id' }),
new CardViewTextItemModel({
label: 'Description',
value: this.taskDetails.description,
key: 'description',
default: 'No description',
multiline: true,
editable: true
}),
new CardViewModel({label: 'Created:', value: this.taskDetails.created, format: 'MMM DD YYYY', key: 'created'}),
new CardViewModel({label: 'Id:', value: this.taskDetails.id, key: 'id'}),
new CardViewModel({label: 'Description:', value: this.taskDetails.description, key: 'description', default: 'No description'}),
new CardViewModel({label: 'Form name:', value: this.formName, key: 'formName', default: 'No form'})
new CardViewTextItemModel({ label: 'Form name', value: this.formName, key: 'formName', default: 'No form' })
];
}
}
@@ -81,7 +85,7 @@ export class ActivitiTaskHeaderComponent implements OnChanges {
}
getTaskStatus(): string {
return this.taskDetails.endDate ? 'Completed' : 'Running';
return this.isCompleted() ? 'Completed' : 'Running';
}
claimTask(taskId: string) {
@@ -91,4 +95,8 @@ export class ActivitiTaskHeaderComponent implements OnChanges {
this.claim.emit(taskId);
});
}
isCompleted() {
return !!this.taskDetails.endDate;
}
}

View File

@@ -24,7 +24,7 @@ describe('NoTaskDetailsTemplateDirective', () => {
let detailsComponent: ActivitiTaskDetailsComponent;
beforeEach(() => {
detailsComponent = new ActivitiTaskDetailsComponent(null, null, null, null, null);
detailsComponent = new ActivitiTaskDetailsComponent(null, null, null, null, null, null);
component = new NoTaskDetailsTemplateDirective(detailsComponent);
});

View File

@@ -562,6 +562,22 @@ describe('Activiti TaskList Service', () => {
});
});
it('should update a task', (done) => {
let taskId = '111';
service.updateTask(taskId, {property: 'value'}).subscribe(
(res: any) => {
done();
}
);
jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200,
contentType: 'application/json',
responseText: JSON.stringify({})
});
});
it('should return the filter if it contains task id', (done) => {
let taskId = '1';
let filterFake = new FilterRepresentationModel({

View File

@@ -375,6 +375,15 @@ export class ActivitiTaskListService {
.catch(err => this.handleError(err));
}
/**
* Update due date
* @param dueDate - the new due date
*/
updateTask(taskId: any, updated): Observable<TaskDetailsModel> {
return Observable.fromPromise(this.apiService.getInstance().activiti.taskApi.updateTask(taskId, updated))
.catch(err => this.handleError(err));
}
private callApiTasksFiltered(requestNode: TaskQueryRequestRepresentationModel) {
return this.apiService.getInstance().activiti.taskApi.listTasks(requestNode);
}

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 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';

View File

@@ -77,6 +77,7 @@ export class AppComponent {
| needHelpLink | string | | It will change the url of the NEED HELP link in the footer |
| registerLink | string | | It will change the url of the REGISTER link in the footer |
| logoImageUrl | string | Alfresco logo image | To change the logo image with a customised image |
| copyrightText | string | © 2016 Alfresco Software, Inc. All Rights Reserved. | The copyright text below the login box |
| backgroundImageUrl | string | Alfresco background image | To change the background image with a customised image |
| fieldsValidation | { [key: string]: any; }, extra?: { [key: string]: any; } | | Use it to customise the validation rules of the login form |
| showRememberMe | boolean | false | Toggle `Remember me` checkbox visibility |

View File

@@ -63,8 +63,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, '../../ng2-alfresco-core/styles')]
}
}],
exclude: [/node_modules/, /bundles/, /dist/, /demo/]
},
{

View File

@@ -107,7 +107,7 @@
</form>
</div>
<div class="copyright">
&copy; 2016 Alfresco Software, Inc. All Rights Reserved.
<div class="copyright" data-automation-id="login-copyright">
{{ copyrightText }}
</div>
</div>

View File

@@ -157,6 +157,22 @@ describe('AlfrescoLogin', () => {
expect(element.querySelector('#login-action-register').innerText).toEqual('LOGIN.ACTION.REGISTER');
});
describe('Copyright text', () => {
it('should render the default copyright text', () => {
expect(element.querySelector('[data-automation-id="login-copyright"]')).toBeDefined();
expect(element.querySelector('[data-automation-id="login-copyright"]').innerText).toEqual('© 2016 Alfresco Software, Inc. All Rights Reserved.');
});
it('should render the customised copyright text', () => {
component.copyrightText = 'customised';
fixture.detectChanges();
expect(element.querySelector('[data-automation-id="login-copyright"]')).toBeDefined();
expect(element.querySelector('[data-automation-id="login-copyright"]').innerText).toEqual('customised');
});
});
it('should render user and password input fields with default values', () => {
expect(element.querySelector('form')).toBeDefined();
expect(element.querySelector('input[type="password"]')).toBeDefined();

View File

@@ -63,6 +63,9 @@ export class AlfrescoLoginComponent implements OnInit {
@Input()
backgroundImageUrl: string = require('../assets/images/background.svg');
@Input()
copyrightText: string = '© 2016 Alfresco Software, Inc. All Rights Reserved.';
@Input()
providers: string;