diff --git a/demo-shell-ng2/app/components/login/login-demo.component.html b/demo-shell-ng2/app/components/login/login-demo.component.html index 4203567ec1..05c0f2bbcb 100644 --- a/demo-shell-ng2/app/components/login/login-demo.component.html +++ b/demo-shell-ng2/app/components/login/login-demo.component.html @@ -23,7 +23,7 @@

@@ -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)">
@@ -67,7 +68,7 @@

diff --git a/demo-shell-ng2/app/components/login/login-demo.component.ts b/demo-shell-ng2/app/components/login/login-demo.component.ts index 010f7b83b6..9009b4982e 100644 --- a/demo-shell-ng2/app/components/login/login-demo.component.ts +++ b/demo-shell-ng2/app/components/login/login-demo.component.ts @@ -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'; diff --git a/ng2-components/ng2-activiti-tasklist/config/webpack.common.js b/ng2-components/ng2-activiti-tasklist/config/webpack.common.js index b872863854..7902fbfc1c 100644 --- a/ng2-components/ng2-activiti-tasklist/config/webpack.common.js +++ b/ng2-components/ng2-activiti-tasklist/config/webpack.common.js @@ -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/] }, { diff --git a/ng2-components/ng2-activiti-tasklist/index.ts b/ng2-components/ng2-activiti-tasklist/index.ts index 345da37133..1f193854e7 100644 --- a/ng2-components/ng2-activiti-tasklist/index.ts +++ b/ng2-components/ng2-activiti-tasklist/index.ts @@ -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, diff --git a/ng2-components/ng2-activiti-tasklist/src/components/activiti-task-details.component.css b/ng2-components/ng2-activiti-tasklist/src/components/activiti-task-details.component.css index 6ed0c48abd..9ed5ae7394 100644 --- a/ng2-components/ng2-activiti-tasklist/src/components/activiti-task-details.component.css +++ b/ng2-components/ng2-activiti-tasklist/src/components/activiti-task-details.component.css @@ -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; diff --git a/ng2-components/ng2-activiti-tasklist/src/components/activiti-task-details.component.ts b/ng2-components/ng2-activiti-tasklist/src/components/activiti-task-details.component.ts index d3865518ab..83d4eeedc1 100644 --- a/ng2-components/ng2-activiti-tasklist/src/components/activiti-task-details.component.ts +++ b/ng2-components/ng2-activiti-tasklist/src/components/activiti-task-details.component.ts @@ -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; /** - * 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 diff --git a/ng2-components/ng2-activiti-tasklist/src/components/activiti-task-header.component.css b/ng2-components/ng2-activiti-tasklist/src/components/activiti-task-header.component.css deleted file mode 100644 index 6b3b1c13f4..0000000000 --- a/ng2-components/ng2-activiti-tasklist/src/components/activiti-task-header.component.css +++ /dev/null @@ -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; -} diff --git a/ng2-components/ng2-activiti-tasklist/src/components/activiti-task-header.component.html b/ng2-components/ng2-activiti-tasklist/src/components/activiti-task-header.component.html index 8922608c4e..70033b0f08 100644 --- a/ng2-components/ng2-activiti-tasklist/src/components/activiti-task-header.component.html +++ b/ng2-components/ng2-activiti-tasklist/src/components/activiti-task-header.component.html @@ -1,11 +1,13 @@ -
-
+ - + + + - + -
-
+ diff --git a/ng2-components/ng2-activiti-tasklist/src/components/activiti-task-header.component.scss b/ng2-components/ng2-activiti-tasklist/src/components/activiti-task-header.component.scss new file mode 100644 index 0000000000..488457f6ca --- /dev/null +++ b/ng2-components/ng2-activiti-tasklist/src/components/activiti-task-header.component.scss @@ -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); + } +} \ No newline at end of file diff --git a/ng2-components/ng2-activiti-tasklist/src/components/activiti-task-header.component.spec.ts b/ng2-components/ng2-activiti-tasklist/src/components/activiti-task-header.component.spec.ts index e6ffa9cbdf..a7780a4e08 100644 --- a/ng2-components/ng2-activiti-tasklist/src/components/activiti-task-header.component.spec.ts +++ b/ng2-components/ng2-activiti-tasklist/src/components/activiti-task-header.component.spec.ts @@ -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; + 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'); }); diff --git a/ng2-components/ng2-activiti-tasklist/src/components/activiti-task-header.component.ts b/ng2-components/ng2-activiti-tasklist/src/components/activiti-task-header.component.ts index e3415109a4..539326579c 100644 --- a/ng2-components/ng2-activiti-tasklist/src/components/activiti-task-header.component.ts +++ b/ng2-components/ng2-activiti-tasklist/src/components/activiti-task-header.component.ts @@ -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 = new EventEmitter(); - 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 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: '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 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; + } } diff --git a/ng2-components/ng2-activiti-tasklist/src/components/no-task-detail-template.component.spec.ts b/ng2-components/ng2-activiti-tasklist/src/components/no-task-detail-template.component.spec.ts index f47f34faf9..a5e88d3eec 100644 --- a/ng2-components/ng2-activiti-tasklist/src/components/no-task-detail-template.component.spec.ts +++ b/ng2-components/ng2-activiti-tasklist/src/components/no-task-detail-template.component.spec.ts @@ -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); }); diff --git a/ng2-components/ng2-activiti-tasklist/src/services/activiti-tasklist.service.spec.ts b/ng2-components/ng2-activiti-tasklist/src/services/activiti-tasklist.service.spec.ts index 0ebaa7e91a..a5913a5d73 100644 --- a/ng2-components/ng2-activiti-tasklist/src/services/activiti-tasklist.service.spec.ts +++ b/ng2-components/ng2-activiti-tasklist/src/services/activiti-tasklist.service.spec.ts @@ -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({ diff --git a/ng2-components/ng2-activiti-tasklist/src/services/activiti-tasklist.service.ts b/ng2-components/ng2-activiti-tasklist/src/services/activiti-tasklist.service.ts index 61b9adec5c..5000d9a8a3 100644 --- a/ng2-components/ng2-activiti-tasklist/src/services/activiti-tasklist.service.ts +++ b/ng2-components/ng2-activiti-tasklist/src/services/activiti-tasklist.service.ts @@ -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 { + 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); } diff --git a/ng2-components/ng2-alfresco-core/README.md b/ng2-components/ng2-alfresco-core/README.md index 1ac570ba82..6d666b238b 100644 --- a/ng2-components/ng2-alfresco-core/README.md +++ b/ng2-components/ng2-alfresco-core/README.md @@ -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 + [properties]="[{label: 'My Label', value: 'My value'}]" + [editable]="false"> ``` +![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: '', + 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 diff --git a/ng2-components/ng2-alfresco-core/config/webpack.common.js b/ng2-components/ng2-alfresco-core/config/webpack.common.js index 5c1dc42fe3..c4f1184dd6 100644 --- a/ng2-components/ng2-alfresco-core/config/webpack.common.js +++ b/ng2-components/ng2-alfresco-core/config/webpack.common.js @@ -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/] }, { diff --git a/ng2-components/ng2-alfresco-core/docs/assets/adf-custom-view.png b/ng2-components/ng2-alfresco-core/docs/assets/adf-custom-view.png index 41ed32afb9..4b99842c33 100644 Binary files a/ng2-components/ng2-alfresco-core/docs/assets/adf-custom-view.png and b/ng2-components/ng2-alfresco-core/docs/assets/adf-custom-view.png differ diff --git a/ng2-components/ng2-alfresco-core/index.ts b/ng2-components/ng2-alfresco-core/index.ts index d6cd96016a..d185691199 100644 --- a/ng2-components/ng2-alfresco-core/index.ts +++ b/ng2-components/ng2-alfresco-core/index.ts @@ -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'; diff --git a/ng2-components/ng2-alfresco-core/src/components/view/adf-card-view-content-proxy.directive.ts b/ng2-components/ng2-alfresco-core/src/components/view/adf-card-view-content-proxy.directive.ts new file mode 100644 index 0000000000..00e65a40f7 --- /dev/null +++ b/ng2-components/ng2-alfresco-core/src/components/view/adf-card-view-content-proxy.directive.ts @@ -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) { } +} diff --git a/ng2-components/ng2-alfresco-core/src/components/view/adf-card-view-dateitem.component.html b/ng2-components/ng2-alfresco-core/src/components/view/adf-card-view-dateitem.component.html new file mode 100644 index 0000000000..896ff9a5aa --- /dev/null +++ b/ng2-components/ng2-alfresco-core/src/components/view/adf-card-view-dateitem.component.html @@ -0,0 +1,23 @@ +
{{ property.label }}
+
+ + {{ property.displayValue }} + + + {{ property.displayValue }} + + + + + +
diff --git a/ng2-components/ng2-alfresco-core/src/components/view/adf-card-view-dateitem.component.scss b/ng2-components/ng2-alfresco-core/src/components/view/adf-card-view-dateitem.component.scss new file mode 100644 index 0000000000..f10e76ee69 --- /dev/null +++ b/ng2-components/ng2-alfresco-core/src/components/view/adf-card-view-dateitem.component.scss @@ -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; + } + } +} \ No newline at end of file diff --git a/ng2-components/ng2-alfresco-core/src/components/view/adf-card-view-dateitem.component.spec.ts b/ng2-components/ng2-alfresco-core/src/components/view/adf-card-view-dateitem.component.spec.ts new file mode 100644 index 0000000000..f491f7a9f8 --- /dev/null +++ b/ng2-components/ng2-alfresco-core/src/components/view/adf-card-view-dateitem.component.spec.ts @@ -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; + 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 = { + 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); + }); +}); diff --git a/ng2-components/ng2-alfresco-core/src/components/view/adf-card-view-dateitem.component.ts b/ng2-components/ng2-alfresco-core/src/components/view/adf-card-view-dateitem.component.ts new file mode 100644 index 0000000000..40f4822bc9 --- /dev/null +++ b/ng2-components/ng2-alfresco-core/src/components/view/adf-card-view-dateitem.component.ts @@ -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; + + 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 }); + } +} diff --git a/ng2-components/ng2-alfresco-core/src/components/view/adf-card-view-item-dispatcher.component.spec.ts b/ng2-components/ng2-alfresco-core/src/components/view/adf-card-view-item-dispatcher.component.spec.ts new file mode 100644 index 0000000000..b0e598a3cc --- /dev/null +++ b/ng2-components/ng2-alfresco-core/src/components/view/adf-card-view-item-dispatcher.component.spec.ts @@ -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: '
Hey I am shiny!
' +}) +export class CardViewShinyCustomElementItemComponent { + @Input() property: CardViewItem; + @Input() editable: boolean; +} + +describe('CardViewItemDispatcherComponent', () => { + + let fixture: ComponentFixture; + 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 = { + 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(); + }); + }); + }); +}); diff --git a/ng2-components/ng2-alfresco-core/src/components/view/adf-card-view-item-dispatcher.component.ts b/ng2-components/ng2-alfresco-core/src/components/view/adf-card-view-item-dispatcher.component.ts new file mode 100644 index 0000000000..2028708fdf --- /dev/null +++ b/ng2-components/ng2-alfresco-core/src/components/view/adf-card-view-item-dispatcher.component.ts @@ -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: '' +}) +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 = >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); + } + } +} diff --git a/ng2-components/ng2-alfresco-core/src/components/view/adf-card-view-textitem.component.html b/ng2-components/ng2-alfresco-core/src/components/view/adf-card-view-textitem.component.html new file mode 100644 index 0000000000..db42631255 --- /dev/null +++ b/ng2-components/ng2-alfresco-core/src/components/view/adf-card-view-textitem.component.html @@ -0,0 +1,39 @@ +
{{ property.label }}
+
+ + {{ property.displayValue }} + + +
+ {{ property.displayValue }} + create +
+
+ + + + + done + clear +
+
+
\ No newline at end of file diff --git a/ng2-components/ng2-alfresco-core/src/components/view/adf-card-view-textitem.component.scss b/ng2-components/ng2-alfresco-core/src/components/view/adf-card-view-textitem.component.scss new file mode 100644 index 0000000000..e9111148dc --- /dev/null +++ b/ng2-components/ng2-alfresco-core/src/components/view/adf-card-view-textitem.component.scss @@ -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; + } +} \ No newline at end of file diff --git a/ng2-components/ng2-alfresco-core/src/components/view/adf-card-view-textitem.component.spec.ts b/ng2-components/ng2-alfresco-core/src/components/view/adf-card-view-textitem.component.spec.ts new file mode 100644 index 0000000000..621d5ff77d --- /dev/null +++ b/ng2-components/ng2-alfresco-core/src/components/view/adf-card-view-textitem.component.spec.ts @@ -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; + 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 = { + 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); + }); +}); diff --git a/ng2-components/ng2-alfresco-core/src/components/view/adf-card-view-textitem.component.ts b/ng2-components/ng2-alfresco-core/src/components/view/adf-card-view-textitem.component.ts new file mode 100644 index 0000000000..55fb177469 --- /dev/null +++ b/ng2-components/ng2-alfresco-core/src/components/view/adf-card-view-textitem.component.ts @@ -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 }); + } +} diff --git a/ng2-components/ng2-alfresco-core/src/components/view/adf-card-view.component.css b/ng2-components/ng2-alfresco-core/src/components/view/adf-card-view.component.css deleted file mode 100644 index 1e122ae577..0000000000 --- a/ng2-components/ng2-alfresco-core/src/components/view/adf-card-view.component.css +++ /dev/null @@ -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; -} diff --git a/ng2-components/ng2-alfresco-core/src/components/view/adf-card-view.component.html b/ng2-components/ng2-alfresco-core/src/components/view/adf-card-view.component.html index 891a7b2ff0..b74f8d0d1c 100644 --- a/ng2-components/ng2-alfresco-core/src/components/view/adf-card-view.component.html +++ b/ng2-components/ng2-alfresco-core/src/components/view/adf-card-view.component.html @@ -1,6 +1,7 @@ -
-
-
{{ property.label }}
-
{{ getPropertyValue(property) }}
-
+
+ +
+ +
+
diff --git a/ng2-components/ng2-alfresco-core/src/components/view/adf-card-view.component.scss b/ng2-components/ng2-alfresco-core/src/components/view/adf-card-view.component.scss new file mode 100644 index 0000000000..d4e693ef24 --- /dev/null +++ b/ng2-components/ng2-alfresco-core/src/components/view/adf-card-view.component.scss @@ -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; + } +} diff --git a/ng2-components/ng2-alfresco-core/src/components/view/adf-card-view.component.spec.ts b/ng2-components/ng2-alfresco-core/src/components/view/adf-card-view.component.spec.ts index 0a882630f2..1bb1d6e5e0 100644 --- a/ng2-components/ng2-alfresco-core/src/components/view/adf-card-view.component.spec.ts +++ b/ng2-components/ng2-alfresco-core/src/components/view/adf-card-view.component.spec.ts @@ -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'); }); - })); }); diff --git a/ng2-components/ng2-alfresco-core/src/components/view/adf-card-view.component.ts b/ng2-components/ng2-alfresco-core/src/components/view/adf-card-view.component.ts index da0c931bff..0aca314346 100644 --- a/ng2-components/ng2-alfresco-core/src/components/view/adf-card-view.component.ts +++ b/ng2-components/ng2-alfresco-core/src/components/view/adf-card-view.component.ts @@ -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; } diff --git a/ng2-components/ng2-alfresco-core/src/components/view/card-view.module.ts b/ng2-components/ng2-alfresco-core/src/components/view/card-view.module.ts index 3a970e98e8..10ac39bdc3 100644 --- a/ng2-components/ng2-alfresco-core/src/components/view/card-view.module.ts +++ b/ng2-components/ng2-alfresco-core/src/components/view/card-view.module.ts @@ -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 diff --git a/ng2-components/ng2-alfresco-core/src/interface/card-view-item.interface.ts b/ng2-components/ng2-alfresco-core/src/interface/card-view-item.interface.ts new file mode 100644 index 0000000000..b3496f234e --- /dev/null +++ b/ng2-components/ng2-alfresco-core/src/interface/card-view-item.interface.ts @@ -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; +} diff --git a/ng2-components/ng2-alfresco-core/src/models/card-view.model.ts b/ng2-components/ng2-alfresco-core/src/models/card-view-baseitem.model.ts similarity index 72% rename from ng2-components/ng2-alfresco-core/src/models/card-view.model.ts rename to ng2-components/ng2-alfresco-core/src/models/card-view-baseitem.model.ts index 9c804d1413..4da54a4adc 100644 --- a/ng2-components/ng2-alfresco-core/src/models/card-view.model.ts +++ b/ng2-components/ng2-alfresco-core/src/models/card-view-baseitem.model.ts @@ -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; } } diff --git a/ng2-components/ng2-alfresco-core/src/models/card-view-dateitem.model.ts b/ng2-components/ng2-alfresco-core/src/models/card-view-dateitem.model.ts new file mode 100644 index 0000000000..4ff4f6ef28 --- /dev/null +++ b/ng2-components/ng2-alfresco-core/src/models/card-view-dateitem.model.ts @@ -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); + } + } +} diff --git a/ng2-components/ng2-alfresco-core/src/models/card-view-textitem.model.ts b/ng2-components/ng2-alfresco-core/src/models/card-view-textitem.model.ts new file mode 100644 index 0000000000..fccfddad5a --- /dev/null +++ b/ng2-components/ng2-alfresco-core/src/models/card-view-textitem.model.ts @@ -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; + } +} diff --git a/ng2-components/ng2-alfresco-core/src/models/index.ts b/ng2-components/ng2-alfresco-core/src/models/index.ts index fff2631f30..956380ce41 100644 --- a/ng2-components/ng2-alfresco-core/src/models/index.ts +++ b/ng2-components/ng2-alfresco-core/src/models/index.ts @@ -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'; diff --git a/ng2-components/ng2-alfresco-core/src/services/adf-card-view-update.service.ts b/ng2-components/ng2-alfresco-core/src/services/adf-card-view-update.service.ts new file mode 100644 index 0000000000..3821af74ab --- /dev/null +++ b/ng2-components/ng2-alfresco-core/src/services/adf-card-view-update.service.ts @@ -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(); + + // Observable streams + public itemUpdated$ = this.itemUpdatedSource.asObservable(); + + update(property: CardViewBaseItemModel, changed: any) { + this.itemUpdatedSource.next({ + target: property, + changed + }); + } +} diff --git a/ng2-components/ng2-alfresco-core/src/services/index.ts b/ng2-components/ng2-alfresco-core/src/services/index.ts index 366db8fbb2..eb4bbb3393 100644 --- a/ng2-components/ng2-alfresco-core/src/services/index.ts +++ b/ng2-components/ng2-alfresco-core/src/services/index.ts @@ -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'; diff --git a/ng2-components/ng2-alfresco-login/README.md b/ng2-components/ng2-alfresco-login/README.md index 023f0ef82f..e4ca4d87ba 100644 --- a/ng2-components/ng2-alfresco-login/README.md +++ b/ng2-components/ng2-alfresco-login/README.md @@ -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 | diff --git a/ng2-components/ng2-alfresco-login/config/webpack.common.js b/ng2-components/ng2-alfresco-login/config/webpack.common.js index f90fe35645..53caec66fd 100644 --- a/ng2-components/ng2-alfresco-login/config/webpack.common.js +++ b/ng2-components/ng2-alfresco-login/config/webpack.common.js @@ -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/] }, { diff --git a/ng2-components/ng2-alfresco-login/src/components/alfresco-login.component.html b/ng2-components/ng2-alfresco-login/src/components/alfresco-login.component.html index 09851f9928..e862f8875e 100644 --- a/ng2-components/ng2-alfresco-login/src/components/alfresco-login.component.html +++ b/ng2-components/ng2-alfresco-login/src/components/alfresco-login.component.html @@ -107,7 +107,7 @@
- diff --git a/ng2-components/ng2-alfresco-login/src/components/alfresco-login.component.spec.ts b/ng2-components/ng2-alfresco-login/src/components/alfresco-login.component.spec.ts index df24628fcb..18e2256122 100644 --- a/ng2-components/ng2-alfresco-login/src/components/alfresco-login.component.spec.ts +++ b/ng2-components/ng2-alfresco-login/src/components/alfresco-login.component.spec.ts @@ -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(); diff --git a/ng2-components/ng2-alfresco-login/src/components/alfresco-login.component.ts b/ng2-components/ng2-alfresco-login/src/components/alfresco-login.component.ts index f430705f8f..cd11040e1a 100644 --- a/ng2-components/ng2-alfresco-login/src/components/alfresco-login.component.ts +++ b/ng2-components/ng2-alfresco-login/src/components/alfresco-login.component.ts @@ -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;