Commit 1894afc0521954895e6b817afa85c8c4a51e8d5e

Authored by Michel Felipe
2 parents 0e6cfa00 5f9ef2ae

Merge branch 'create-article' into 'master'

Basic form to create and edit articles

See merge request !11
@@ -34,7 +34,9 @@ @@ -34,7 +34,9 @@
34 "angular-dynamic-locale": "^0.1.30", 34 "angular-dynamic-locale": "^0.1.30",
35 "angular-i18n": "^1.5.0", 35 "angular-i18n": "^1.5.0",
36 "angular-load": "^0.4.1", 36 "angular-load": "^0.4.1",
37 - "angular-translate-interpolation-messageformat": "^2.10.0" 37 + "angular-translate-interpolation-messageformat": "^2.10.0",
  38 + "ng-ckeditor": "^0.2.1",
  39 + "ckeditor": "^4.5.8"
38 }, 40 },
39 "devDependencies": { 41 "devDependencies": {
40 "angular-mocks": "~1.5.0" 42 "angular-mocks": "~1.5.0"
src/app/article/article.html
@@ -4,6 +4,9 @@ @@ -4,6 +4,9 @@
4 </div> 4 </div>
5 5
6 <div class="sub-header clearfix"> 6 <div class="sub-header clearfix">
  7 + <a href="#" class="btn btn-default btn-xs" ui-sref="main.cmsEdit({profile: ctrl.profile.identifier, id: ctrl.article.id})">
  8 + <i class="fa fa-pencil-square-o fa-fw fa-lg"></i> {{"article.actions.edit" | translate}}
  9 + </a>
7 <div class="page-info pull-right small text-muted"> 10 <div class="page-info pull-right small text-muted">
8 <span class="time"> 11 <span class="time">
9 <i class="fa fa-clock-o"></i> <span am-time-ago="ctrl.article.created_at | dateFormat"></span> 12 <i class="fa fa-clock-o"></i> <span am-time-ago="ctrl.article.created_at | dateFormat"></span>
src/app/article/basic-editor.component.spec.ts
@@ -1,55 +0,0 @@ @@ -1,55 +0,0 @@
1 -import {quickCreateComponent} from "../../spec/helpers";  
2 -import {BasicEditorComponent} from "./basic-editor.component";  
3 -  
4 -  
5 -describe("Article BasicEditor", () => {  
6 -  
7 - let $rootScope: ng.IRootScopeService;  
8 - let $q: ng.IQService;  
9 - let articleServiceMock: any;  
10 - let profileServiceMock: any;  
11 - let $state: any;  
12 - let profile = { id: 1 };  
13 - let notification: any;  
14 -  
15 -  
16 - beforeEach(inject((_$rootScope_: ng.IRootScopeService, _$q_: ng.IQService) => {  
17 - $rootScope = _$rootScope_;  
18 - $q = _$q_;  
19 - }));  
20 -  
21 - beforeEach(() => {  
22 - $state = jasmine.createSpyObj("$state", ["transitionTo"]);  
23 - notification = jasmine.createSpyObj("notification", ["success"]);  
24 - profileServiceMock = jasmine.createSpyObj("profileServiceMock", ["getCurrentProfile"]);  
25 - articleServiceMock = jasmine.createSpyObj("articleServiceMock", ["createInProfile"]);  
26 -  
27 - let getCurrentProfileResponse = $q.defer();  
28 - getCurrentProfileResponse.resolve(profile);  
29 -  
30 - let articleCreate = $q.defer();  
31 - articleCreate.resolve({ data: { path: "path", profile: { identifier: "profile" } } });  
32 -  
33 - profileServiceMock.getCurrentProfile = jasmine.createSpy("getCurrentProfile").and.returnValue(getCurrentProfileResponse.promise);  
34 - articleServiceMock.createInProfile = jasmine.createSpy("createInProfile").and.returnValue(articleCreate.promise);  
35 - });  
36 -  
37 - it("create an article in the current profile when save", done => {  
38 - let component: BasicEditorComponent = new BasicEditorComponent(articleServiceMock, profileServiceMock, $state, notification);  
39 - component.save();  
40 - $rootScope.$apply();  
41 - expect(profileServiceMock.getCurrentProfile).toHaveBeenCalled();  
42 - expect(articleServiceMock.createInProfile).toHaveBeenCalledWith(profile, component.article);  
43 - done();  
44 - });  
45 -  
46 - it("got to the new article page and display an alert when saving sucessfully", done => {  
47 - let component: BasicEditorComponent = new BasicEditorComponent(articleServiceMock, profileServiceMock, $state, notification);  
48 - component.save();  
49 - $rootScope.$apply();  
50 - expect($state.transitionTo).toHaveBeenCalledWith("main.profile.page", { page: "path", profile: "profile" });  
51 - expect(notification.success).toHaveBeenCalled();  
52 - done();  
53 - });  
54 -  
55 -});  
src/app/article/basic-editor.component.ts
@@ -1,35 +0,0 @@ @@ -1,35 +0,0 @@
1 -import {StateConfig, Component, Inject, provide} from 'ng-forward';  
2 -import {ArticleService} from "../../lib/ng-noosfero-api/http/article.service";  
3 -import {ProfileService} from "../../lib/ng-noosfero-api/http/profile.service";  
4 -import {NotificationService} from "../shared/services/notification.service.ts";  
5 -  
6 -@Component({  
7 - selector: 'article-basic-editor',  
8 - templateUrl: "app/article/basic-editor.html",  
9 - providers: [  
10 - provide('articleService', { useClass: ArticleService }),  
11 - provide('profileService', { useClass: ProfileService }),  
12 - provide('notification', { useClass: NotificationService })  
13 - ]  
14 -})  
15 -@Inject(ArticleService, ProfileService, "$state", NotificationService)  
16 -export class BasicEditorComponent {  
17 -  
18 - article: noosfero.Article = <noosfero.Article>{};  
19 -  
20 - constructor(private articleService: ArticleService,  
21 - private profileService: ProfileService,  
22 - private $state: ng.ui.IStateService,  
23 - private notification: NotificationService) { }  
24 -  
25 - save() {  
26 - this.profileService.getCurrentProfile().then((profile: noosfero.Profile) => {  
27 - return this.articleService.createInProfile(profile, this.article);  
28 - }).then((response: noosfero.RestResult<noosfero.Article>) => {  
29 - let article = (<noosfero.Article>response.data);  
30 - this.$state.transitionTo('main.profile.page', { page: article.path, profile: article.profile.identifier });  
31 - this.notification.success({ title: "Good job!", message: "Article saved!" });  
32 - });  
33 - }  
34 -  
35 -}  
src/app/article/basic-editor.html
@@ -1,11 +0,0 @@ @@ -1,11 +0,0 @@
1 -<form>  
2 - <div class="form-group">  
3 - <label for="titleInput">Title</label>  
4 - <input type="text" class="form-control" id="titleInput" placeholder="title" ng-model="vm.article.name">  
5 - </div>  
6 - <div class="form-group">  
7 - <label for="bodyInput">Text</label>  
8 - <textarea class="form-control" id="bodyInput" rows="10" ng-model="vm.article.body"></textarea>  
9 - </div>  
10 - <button type="submit" class="btn btn-default" ng-click="vm.save()">Save</button>  
11 -</form>  
src/app/article/basic-editor/basic-editor.component.spec.ts 0 → 100644
@@ -0,0 +1,84 @@ @@ -0,0 +1,84 @@
  1 +import {quickCreateComponent} from "../../../spec/helpers";
  2 +import {BasicEditorComponent} from "./basic-editor.component";
  3 +
  4 +
  5 +describe("Article BasicEditor", () => {
  6 +
  7 + let $rootScope: ng.IRootScopeService;
  8 + let $q: ng.IQService;
  9 + let articleServiceMock: any;
  10 + let profileServiceMock: any;
  11 + let $state: any;
  12 + let $stateParams: any;
  13 + let $window: any;
  14 + let profile = { id: 1 };
  15 + let notification: any;
  16 +
  17 +
  18 + beforeEach(inject((_$rootScope_: ng.IRootScopeService, _$q_: ng.IQService) => {
  19 + $rootScope = _$rootScope_;
  20 + $q = _$q_;
  21 + }));
  22 +
  23 + beforeEach(() => {
  24 + $window = jasmine.createSpyObj("$window", ["back"]);
  25 + $state = jasmine.createSpyObj("$state", ["go"]);
  26 + notification = jasmine.createSpyObj("notification", ["success"]);
  27 + profileServiceMock = jasmine.createSpyObj("profileServiceMock", ["setCurrentProfileByIdentifier"]);
  28 + articleServiceMock = jasmine.createSpyObj("articleServiceMock", ["createInParent", "updateArticle", "get"]);
  29 +
  30 + $stateParams = { profile: "profile" };
  31 +
  32 + let setCurrentProfileByIdentifierResponse = $q.defer();
  33 + setCurrentProfileByIdentifierResponse.resolve(profile);
  34 +
  35 + let articleCreate = $q.defer();
  36 + articleCreate.resolve({ data: { path: "path", profile: { identifier: "profile" } } });
  37 +
  38 + let articleGet = $q.defer();
  39 + articleGet.resolve({ data: { path: "parent-path", profile: { identifier: "profile" } } });
  40 +
  41 + profileServiceMock.setCurrentProfileByIdentifier = jasmine.createSpy("setCurrentProfileByIdentifier").and.returnValue(setCurrentProfileByIdentifierResponse.promise);
  42 + articleServiceMock.createInParent = jasmine.createSpy("createInParent").and.returnValue(articleCreate.promise);
  43 + articleServiceMock.updateArticle = jasmine.createSpy("updateArticle").and.returnValue(articleCreate.promise);
  44 + articleServiceMock.get = jasmine.createSpy("get").and.returnValue(articleGet.promise);
  45 + });
  46 +
  47 + it("create an article in the current profile when save", done => {
  48 + $stateParams['parent_id'] = 1;
  49 + let component: BasicEditorComponent = new BasicEditorComponent(articleServiceMock, profileServiceMock, $state, notification, $stateParams, $window);
  50 + component.save();
  51 + $rootScope.$apply();
  52 + expect(profileServiceMock.setCurrentProfileByIdentifier).toHaveBeenCalled();
  53 + expect(articleServiceMock.createInParent).toHaveBeenCalledWith(1, component.article);
  54 + done();
  55 + });
  56 +
  57 + it("got to the new article page and display an alert when saving sucessfully", done => {
  58 + let component: BasicEditorComponent = new BasicEditorComponent(articleServiceMock, profileServiceMock, $state, notification, $stateParams, $window);
  59 + component.save();
  60 + $rootScope.$apply();
  61 + expect($state.go).toHaveBeenCalledWith("main.profile.page", { page: "path", profile: "profile" });
  62 + expect(notification.success).toHaveBeenCalled();
  63 + done();
  64 + });
  65 +
  66 + it("go back when cancel article edition", done => {
  67 + let component: BasicEditorComponent = new BasicEditorComponent(articleServiceMock, profileServiceMock, $state, notification, $stateParams, $window);
  68 + $window.history = { back: jasmine.createSpy('back') };
  69 + component.cancel();
  70 + expect($window.history.back).toHaveBeenCalled();
  71 + done();
  72 + });
  73 +
  74 + it("edit existing article when save", done => {
  75 + $stateParams['parent_id'] = null;
  76 + $stateParams['id'] = 2;
  77 + let component: BasicEditorComponent = new BasicEditorComponent(articleServiceMock, profileServiceMock, $state, notification, $stateParams, $window);
  78 + component.save();
  79 + $rootScope.$apply();
  80 + expect(articleServiceMock.updateArticle).toHaveBeenCalledWith(component.article);
  81 + done();
  82 + });
  83 +
  84 +});
src/app/article/basic-editor/basic-editor.component.ts 0 → 100644
@@ -0,0 +1,69 @@ @@ -0,0 +1,69 @@
  1 +import {StateConfig, Component, Inject, provide} from 'ng-forward';
  2 +import {ArticleService} from "../../../lib/ng-noosfero-api/http/article.service";
  3 +import {ProfileService} from "../../../lib/ng-noosfero-api/http/profile.service";
  4 +import {NotificationService} from "../../shared/services/notification.service.ts";
  5 +
  6 +@Component({
  7 + selector: 'article-basic-editor',
  8 + templateUrl: "app/article/basic-editor/basic-editor.html",
  9 + providers: [
  10 + provide('articleService', { useClass: ArticleService }),
  11 + provide('profileService', { useClass: ProfileService }),
  12 + provide('notification', { useClass: NotificationService })
  13 + ]
  14 +})
  15 +@Inject(ArticleService, ProfileService, "$state", NotificationService, "$stateParams", "$window")
  16 +export class BasicEditorComponent {
  17 +
  18 + article: noosfero.Article = <noosfero.Article>{ type: "TextArticle" };
  19 + parent: noosfero.Article = <noosfero.Article>{};
  20 +
  21 + id: number;
  22 + parentId: number;
  23 + profileIdentifier: string;
  24 +
  25 + constructor(private articleService: ArticleService,
  26 + private profileService: ProfileService,
  27 + private $state: ng.ui.IStateService,
  28 + private notification: NotificationService,
  29 + private $stateParams: ng.ui.IStateParamsService,
  30 + private $window: ng.IWindowService) {
  31 +
  32 + this.parentId = this.$stateParams['parent_id'];
  33 + this.profileIdentifier = this.$stateParams["profile"];
  34 + this.id = this.$stateParams['id'];
  35 +
  36 + if (this.parentId) {
  37 + this.articleService.get(this.parentId).then((result: noosfero.RestResult<noosfero.Article>) => {
  38 + this.parent = result.data;
  39 + });
  40 + }
  41 + if (this.id) {
  42 + this.articleService.get(this.id).then((result: noosfero.RestResult<noosfero.Article>) => {
  43 + this.article = result.data;
  44 + this.article.name = this.article.title; // FIXME
  45 + });
  46 + }
  47 + }
  48 +
  49 + save() {
  50 + this.profileService.setCurrentProfileByIdentifier(this.profileIdentifier).then((profile: noosfero.Profile) => {
  51 + if (this.id) {
  52 + return this.articleService.updateArticle(this.article);
  53 + } else {
  54 + return this.articleService.createInParent(this.parentId, this.article);
  55 + }
  56 + }).then((response: noosfero.RestResult<noosfero.Article>) => {
  57 + let article = (<noosfero.Article>response.data);
  58 + this.$state.go('main.profile.page', { page: article.path, profile: article.profile.identifier });
  59 + this.notification.success({ title: "article.basic_editor.success.title", message: "article.basic_editor.success.message" });
  60 + }).catch(() => {
  61 + this.notification.error({ message: "article.basic_editor.save.failed" });
  62 + });
  63 + }
  64 +
  65 + cancel() {
  66 + this.$window.history.back();
  67 + }
  68 +
  69 +}
src/app/article/basic-editor/basic-editor.html 0 → 100644
@@ -0,0 +1,34 @@ @@ -0,0 +1,34 @@
  1 +<div class="basic-editor">
  2 + <div class="row">
  3 + <div class="col-md-1"></div>
  4 + <div class="col-md-8">
  5 + <form>
  6 + <div class="form-group">
  7 + <label for="titleInput">{{"article.basic_editor.title" | translate}}</label>
  8 + <input type="text" class="form-control" id="titleInput" placeholder="{{'article.basic_editor.title' | translate}}" ng-model="vm.article.name">
  9 + </div>
  10 + <div class="form-group">
  11 + <label for="bodyInput">{{"article.basic_editor.body" | translate}}</label>
  12 + <html-editor [(value)]="vm.article.body"></html-editor>
  13 + </div>
  14 + <button type="submit" class="btn btn-default" ng-click="vm.save()">{{"article.basic_editor.save" | translate}}</button>
  15 + <button type="button" class="btn btn-danger" ng-click="vm.cancel()">{{"article.basic_editor.cancel" | translate}}</button>
  16 + </form>
  17 + </div>
  18 + <div class="side-options">
  19 + <div class="visibility panel panel-default">
  20 + <div class="panel-heading">{{"article.basic_editor.visibility" | translate}}</div>
  21 + <div class="panel-body">
  22 + <div>
  23 + <input type="radio" ng-model="vm.article.published" ng-value="true">
  24 + <i class="fa fa-unlock fa-fw"></i> {{"article.basic_editor.visibility.public" | translate}}
  25 + </div>
  26 + <div>
  27 + <input type="radio" ng-model="vm.article.published" ng-value="false">
  28 + <i class="fa fa-lock fa-fw"></i> {{"article.basic_editor.visibility.private" | translate}}
  29 + </div>
  30 + </div>
  31 + </div>
  32 + </div>
  33 + </div>
  34 +</div>
src/app/article/basic-editor/basic-editor.scss 0 → 100644
@@ -0,0 +1,21 @@ @@ -0,0 +1,21 @@
  1 +.basic-editor {
  2 + @extend .container-fluid;
  3 + padding: 0 1%;
  4 +
  5 + .side-options {
  6 + @extend .col-md-3;
  7 + margin-top: 25px;
  8 +
  9 + .visibility {
  10 + .panel-heading {
  11 + background-color: transparent;
  12 + font-weight: bold;
  13 + }
  14 + .panel-body {
  15 + i {
  16 + color: #A5A5A5;
  17 + }
  18 + }
  19 + }
  20 + }
  21 +}
src/app/article/content-viewer/content-viewer-actions.component.spec.ts
1 -import {providers} from 'ng-forward/cjs/testing/providers';  
2 -  
3 import {Input, Component, provide} from 'ng-forward'; 1 import {Input, Component, provide} from 'ng-forward';
4 2
5 import * as helpers from "../../../spec/helpers"; 3 import * as helpers from "../../../spec/helpers";
6 -  
7 -import {ComponentFixture} from 'ng-forward/cjs/testing/test-component-builder'; 4 +import {ComponentTestHelper, createClass} from '../../../spec/component-test-helper';
8 import {ContentViewerActionsComponent} from './content-viewer-actions.component'; 5 import {ContentViewerActionsComponent} from './content-viewer-actions.component';
9 6
10 // this htmlTemplate will be re-used between the container components in this spec file 7 // this htmlTemplate will be re-used between the container components in this spec file
@@ -12,56 +9,57 @@ const htmlTemplate: string = &#39;&lt;content-viewer-actions [article]=&quot;ctrl.article&quot; [ @@ -12,56 +9,57 @@ const htmlTemplate: string = &#39;&lt;content-viewer-actions [article]=&quot;ctrl.article&quot; [
12 9
13 describe('Content Viewer Actions Component', () => { 10 describe('Content Viewer Actions Component', () => {
14 11
15 - beforeEach(() => { 12 + let helper: ComponentTestHelper<ContentViewerActionsComponent>;
16 13
17 - angular.mock.module("templates"); 14 + beforeEach(angular.mock.module("templates"));
18 15
19 - providers((provide: any) => {  
20 - return <any>[  
21 - provide('ProfileService', {  
22 - useValue: helpers.mocks.profileService  
23 - })  
24 - ];  
25 - });  
26 - }); 16 + let providers = [
  17 + provide('ProfileService', {
  18 + useValue: helpers.mocks.profileService
  19 + }),
  20 + provide('ArticleService', {
  21 + useValue: helpers.mocks.articleService
  22 + })
  23 + ].concat(helpers.provideFilters("translateFilter"));
27 24
28 - let buildComponent = (): Promise<ComponentFixture> => {  
29 - return helpers.quickCreateComponent({  
30 - providers: [  
31 - helpers.provideEmptyObjects('Restangular'),  
32 - helpers.provideFilters('translateFilter')  
33 - ], 25 + beforeEach((done) => {
  26 + let cls = createClass({
  27 + template: htmlTemplate,
34 directives: [ContentViewerActionsComponent], 28 directives: [ContentViewerActionsComponent],
35 - template: htmlTemplate 29 + providers: providers
36 }); 30 });
37 - }; 31 + helper = new ComponentTestHelper<ContentViewerActionsComponent>(cls, done);
  32 + });
38 33
39 - it('renders content viewer actions directive', (done: Function) => {  
40 - buildComponent().then((fixture: ComponentFixture) => {  
41 - expect(fixture.debugElement.query('content-viewer-actions').length).toEqual(1); 34 + it('renders content viewer actions directive', () => {
  35 + expect(helper.all("content-viewer-actions").length).toEqual(1);
  36 + });
42 37
43 - done();  
44 - }); 38 + it('return article parent as container when it is not a folder', () => {
  39 + let article = <noosfero.Article>({ id: 1, type: 'TextArticle', parent: { id: 2 } });
  40 + expect(helper.component.getArticleContainer(article)).toEqual(2);
  41 + });
  42 +
  43 + it('return article as container when it is a folder', () => {
  44 + let article = <noosfero.Article>({ id: 1, type: 'Folder' });
  45 + expect(helper.component.getArticleContainer(article)).toEqual(1);
45 }); 46 });
46 47
47 - it('check if profile was loaded', (done: Function) => { 48 + it('return article as container when it is a blog', () => {
  49 + let article = <noosfero.Article>({ id: 1, type: 'Blog' });
  50 + expect(helper.component.getArticleContainer(article)).toEqual(1);
  51 + });
  52 +
  53 + it('check if profile was loaded', () => {
48 let profile: any = { 54 let profile: any = {
49 id: 1, 55 id: 1,
50 identifier: 'the-profile-test', 56 identifier: 'the-profile-test',
51 type: 'Person' 57 type: 'Person'
52 }; 58 };
53 -  
54 helpers.mocks.profileService.getCurrentProfile = () => { 59 helpers.mocks.profileService.getCurrentProfile = () => {
55 return helpers.mocks.promiseResultTemplate(profile); 60 return helpers.mocks.promiseResultTemplate(profile);
56 }; 61 };
57 -  
58 - buildComponent().then((fixture: ComponentFixture) => {  
59 - let contentViewerComp: ContentViewerActionsComponent = fixture.debugElement.componentViewChildren[0].componentInstance;  
60 -  
61 - expect(contentViewerComp.profile).toEqual(jasmine.objectContaining(profile));  
62 -  
63 - done();  
64 - }); 62 + let component = new ContentViewerActionsComponent(<any>helpers.mocks.profileService, <any>helpers.mocks.articleService);
  63 + expect(component.profile).toEqual(jasmine.objectContaining(profile));
65 }); 64 });
66 -  
67 }); 65 });
src/app/article/content-viewer/content-viewer-actions.component.ts
1 import {Component, Inject, provide} from "ng-forward"; 1 import {Component, Inject, provide} from "ng-forward";
2 import {ProfileService} from "../../../lib/ng-noosfero-api/http/profile.service"; 2 import {ProfileService} from "../../../lib/ng-noosfero-api/http/profile.service";
  3 +import {ArticleService} from "../../../lib/ng-noosfero-api/http/article.service";
3 4
4 @Component({ 5 @Component({
5 selector: "content-viewer-actions", 6 selector: "content-viewer-actions",
6 templateUrl: "app/article/content-viewer/navbar-actions.html", 7 templateUrl: "app/article/content-viewer/navbar-actions.html",
7 - providers: [provide('profileService', { useClass: ProfileService })] 8 + providers: [
  9 + provide('profileService', { useClass: ProfileService }),
  10 + provide('articleService', { useClass: ArticleService })
  11 + ]
8 }) 12 })
9 -@Inject(ProfileService) 13 +@Inject(ProfileService, ArticleService)
10 export class ContentViewerActionsComponent { 14 export class ContentViewerActionsComponent {
11 15
12 article: noosfero.Article; 16 article: noosfero.Article;
13 profile: noosfero.Profile; 17 profile: noosfero.Profile;
  18 + parentId: number;
14 19
15 - constructor(profileService: ProfileService) { 20 + constructor(profileService: ProfileService, articleService: ArticleService) {
16 profileService.getCurrentProfile().then((profile: noosfero.Profile) => { 21 profileService.getCurrentProfile().then((profile: noosfero.Profile) => {
17 this.profile = profile; 22 this.profile = profile;
  23 + return articleService.getCurrent();
  24 + }).then((article: noosfero.Article) => {
  25 + this.article = article;
  26 + this.parentId = this.getArticleContainer(article);
18 }); 27 });
19 } 28 }
  29 +
  30 + getArticleContainer(article: noosfero.Article) {
  31 + // FIXME get folder types from api
  32 + if (article.type === "Blog" || article.type === "Folder") {
  33 + return article.id;
  34 + } else if (article.parent) {
  35 + return article.parent.id;
  36 + }
  37 + }
20 } 38 }
src/app/article/content-viewer/content-viewer.component.ts
@@ -28,11 +28,12 @@ export class ContentViewerComponent { @@ -28,11 +28,12 @@ export class ContentViewerComponent {
28 } 28 }
29 29
30 activate() { 30 activate() {
31 - this.profileService.getCurrentProfile().then((profile: noosfero.Profile) => { 31 + this.profileService.getCurrentProfile().then((profile: noosfero.Profile) => {
32 this.profile = profile; 32 this.profile = profile;
33 return this.articleService.getArticleByProfileAndPath(this.profile, this.$stateParams["page"]); 33 return this.articleService.getArticleByProfileAndPath(this.profile, this.$stateParams["page"]);
34 }).then((result: noosfero.RestResult<any>) => { 34 }).then((result: noosfero.RestResult<any>) => {
35 this.article = <noosfero.Article>result.data; 35 this.article = <noosfero.Article>result.data;
  36 + this.articleService.setCurrent(this.article);
36 }); 37 });
37 } 38 }
38 } 39 }
src/app/article/content-viewer/navbar-actions.html
1 <ul class="nav navbar-nav"> 1 <ul class="nav navbar-nav">
2 <li ng-show="vm.profile"> 2 <li ng-show="vm.profile">
3 - <a href="#" role="button" ui-sref="main.profile.cms({profile: vm.profile.identifier})"> 3 + <a ng-show="vm.parentId" href="#" role="button" ui-sref="main.cms({profile: vm.profile.identifier, parent_id: vm.parentId})">
4 <i class="fa fa-file fa-fw fa-lg"></i> {{"navbar.content_viewer_actions.new_post" | translate}} 4 <i class="fa fa-file fa-fw fa-lg"></i> {{"navbar.content_viewer_actions.new_post" | translate}}
5 </a> 5 </a>
6 </li> 6 </li>
src/app/article/index.ts
1 /* Module Index Entry - generated using the script npm run generate-index */ 1 /* Module Index Entry - generated using the script npm run generate-index */
2 export * from "./article-default-view.component"; 2 export * from "./article-default-view.component";
3 -export * from "./basic-editor.component";  
src/app/index.ts
@@ -14,7 +14,7 @@ declare var moment: any; @@ -14,7 +14,7 @@ declare var moment: any;
14 14
15 let noosferoApp: any = bundle("noosferoApp", MainComponent, ["ngAnimate", "ngCookies", "ngStorage", "ngTouch", 15 let noosferoApp: any = bundle("noosferoApp", MainComponent, ["ngAnimate", "ngCookies", "ngStorage", "ngTouch",
16 "ngSanitize", "ngMessages", "ngAria", "restangular", 16 "ngSanitize", "ngMessages", "ngAria", "restangular",
17 - "ui.router", "ui.bootstrap", "toastr", 17 + "ui.router", "ui.bootstrap", "toastr", "ngCkeditor",
18 "angularMoment", "angular.filter", "akoenig.deckgrid", 18 "angularMoment", "angular.filter", "akoenig.deckgrid",
19 "angular-timeline", "duScroll", "oitozero.ngSweetAlert", 19 "angular-timeline", "duScroll", "oitozero.ngSweetAlert",
20 "pascalprecht.translate", "tmh.dynamicLocale", "angularLoad"]).publish(); 20 "pascalprecht.translate", "tmh.dynamicLocale", "angularLoad"]).publish();
src/app/main/main.component.ts
@@ -29,6 +29,7 @@ import {BodyStateClassesService} from &quot;./../layout/services/body-state-classes.s @@ -29,6 +29,7 @@ import {BodyStateClassesService} from &quot;./../layout/services/body-state-classes.s
29 import {Navbar} from "../layout/navbar/navbar"; 29 import {Navbar} from "../layout/navbar/navbar";
30 30
31 import {MainBlockComponent} from "../layout/blocks/main-block/main-block.component"; 31 import {MainBlockComponent} from "../layout/blocks/main-block/main-block.component";
  32 +import {HtmlEditorComponent} from "../shared/components/html-editor/html-editor.component";
32 33
33 34
34 /** 35 /**
@@ -82,7 +83,7 @@ export class EnvironmentContent { @@ -82,7 +83,7 @@ export class EnvironmentContent {
82 directives: [ 83 directives: [
83 ArticleBlogComponent, ArticleViewComponent, BoxesComponent, BlockComponent, 84 ArticleBlogComponent, ArticleViewComponent, BoxesComponent, BlockComponent,
84 EnvironmentComponent, PeopleBlockComponent, 85 EnvironmentComponent, PeopleBlockComponent,
85 - LinkListBlockComponent, CommunitiesBlockComponent, 86 + LinkListBlockComponent, CommunitiesBlockComponent, HtmlEditorComponent,
86 MainBlockComponent, RecentDocumentsBlockComponent, Navbar, ProfileImageBlockComponent, 87 MainBlockComponent, RecentDocumentsBlockComponent, Navbar, ProfileImageBlockComponent,
87 MembersBlockComponent, NoosferoTemplate, DateFormat, RawHTMLBlockComponent 88 MembersBlockComponent, NoosferoTemplate, DateFormat, RawHTMLBlockComponent
88 ], 89 ],
src/app/profile/profile.component.ts
1 import {StateConfig, Component, Inject, provide} from 'ng-forward'; 1 import {StateConfig, Component, Inject, provide} from 'ng-forward';
2 import {ProfileInfoComponent} from './info/profile-info.component'; 2 import {ProfileInfoComponent} from './info/profile-info.component';
3 import {ProfileHomeComponent} from './profile-home.component'; 3 import {ProfileHomeComponent} from './profile-home.component';
4 -import {BasicEditorComponent} from '../article/basic-editor.component'; 4 +import {BasicEditorComponent} from '../article/basic-editor/basic-editor.component';
5 import {ContentViewerComponent} from "../article/content-viewer/content-viewer.component"; 5 import {ContentViewerComponent} from "../article/content-viewer/content-viewer.component";
6 import {ContentViewerActionsComponent} from "../article/content-viewer/content-viewer-actions.component"; 6 import {ContentViewerActionsComponent} from "../article/content-viewer/content-viewer-actions.component";
7 import {ActivitiesComponent} from "./activities/activities.component"; 7 import {ActivitiesComponent} from "./activities/activities.component";
@@ -45,12 +45,24 @@ import {MyProfileComponent} from &quot;./myprofile.component&quot;; @@ -45,12 +45,24 @@ import {MyProfileComponent} from &quot;./myprofile.component&quot;;
45 component: MyProfileComponent 45 component: MyProfileComponent
46 }, 46 },
47 { 47 {
48 - name: 'main.profile.cms',  
49 - url: "^/myprofile/:profile/cms", 48 + name: 'main.cms',
  49 + url: "^/myprofile/:profile/cms?parent_id",
50 component: BasicEditorComponent, 50 component: BasicEditorComponent,
51 views: { 51 views: {
52 - "mainBlockContent": {  
53 - templateUrl: "app/article/basic-editor.html", 52 + "content": {
  53 + templateUrl: "app/article/basic-editor/basic-editor.html",
  54 + controller: BasicEditorComponent,
  55 + controllerAs: "vm"
  56 + }
  57 + }
  58 + },
  59 + {
  60 + name: 'main.cmsEdit',
  61 + url: "^/myprofile/:profile/cms/edit/:id",
  62 + component: BasicEditorComponent,
  63 + views: {
  64 + "content": {
  65 + templateUrl: "app/article/basic-editor/basic-editor.html",
54 controller: BasicEditorComponent, 66 controller: BasicEditorComponent,
55 controllerAs: "vm" 67 controllerAs: "vm"
56 } 68 }
@@ -98,7 +110,7 @@ export class ProfileComponent { @@ -98,7 +110,7 @@ export class ProfileComponent {
98 }).then((response: restangular.IResponse) => { 110 }).then((response: restangular.IResponse) => {
99 this.boxes = response.data.boxes; 111 this.boxes = response.data.boxes;
100 }).catch(() => { 112 }).catch(() => {
101 - $state.transitionTo('main'); 113 + $state.transitionTo('main.environment.home');
102 notificationService.error({ message: "notification.profile.not_found" }); 114 notificationService.error({ message: "notification.profile.not_found" });
103 }); 115 });
104 } 116 }
src/app/shared/components/html-editor/html-editor.component.spec.ts 0 → 100644
@@ -0,0 +1,26 @@ @@ -0,0 +1,26 @@
  1 +import {ComponentTestHelper, createClass} from './../../../../spec/component-test-helper';
  2 +import {HtmlEditorComponent} from "./html-editor.component";
  3 +
  4 +const htmlTemplate: string = '<html-editor [(value)]="ctrl.value" [options]="ctrl.options"></html-editor>';
  5 +
  6 +describe("Components", () => {
  7 + describe("Html Editor Component", () => {
  8 +
  9 + let helper: ComponentTestHelper<HtmlEditorComponent>;
  10 + beforeEach(angular.mock.module("templates"));
  11 +
  12 + beforeEach((done) => {
  13 + let properties = { value: "value" };
  14 + let cls = createClass({
  15 + template: htmlTemplate,
  16 + directives: [HtmlEditorComponent],
  17 + properties: properties
  18 + });
  19 + helper = new ComponentTestHelper<HtmlEditorComponent>(cls, done);
  20 + });
  21 +
  22 + it("render a textarea", () => {
  23 + expect(helper.find("textarea").length).toEqual(1);
  24 + });
  25 + });
  26 +});
src/app/shared/components/html-editor/html-editor.component.ts 0 → 100644
@@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
  1 +import {Component, Input} from "ng-forward";
  2 +
  3 +@Component({
  4 + selector: 'html-editor',
  5 + templateUrl: "app/shared/components/html-editor/html-editor.html",
  6 +})
  7 +export class HtmlEditorComponent {
  8 +
  9 + @Input() options: any = {};
  10 + @Input() value: any;
  11 +}
src/app/shared/components/html-editor/html-editor.html 0 → 100644
@@ -0,0 +1 @@ @@ -0,0 +1 @@
  1 +<textarea ckeditor="ctrl.options" class="form-control" ng-model="ctrl.value"></textarea>
src/app/shared/services/notification.service.ts
@@ -25,7 +25,7 @@ export class NotificationService { @@ -25,7 +25,7 @@ export class NotificationService {
25 25
26 httpError(status: number, data: any): boolean { 26 httpError(status: number, data: any): boolean {
27 this.error({ message: `notification.http_error.${status}.message` }); 27 this.error({ message: `notification.http_error.${status}.message` });
28 - return true; // return true to indicate that the error was already handled 28 + return false; // return true to indicate that the error was already handled
29 } 29 }
30 30
31 success({ 31 success({
src/languages/en.json
@@ -35,5 +35,16 @@ @@ -35,5 +35,16 @@
35 "comment.pagination.more": "More", 35 "comment.pagination.more": "More",
36 "comment.post.success.title": "Good job!", 36 "comment.post.success.title": "Good job!",
37 "comment.post.success.message": "Comment saved!", 37 "comment.post.success.message": "Comment saved!",
38 - "comment.reply": "reply" 38 + "comment.reply": "reply",
  39 + "article.actions.edit": "Edit",
  40 + "article.basic_editor.title": "Title",
  41 + "article.basic_editor.body": "Body",
  42 + "article.basic_editor.save": "Save",
  43 + "article.basic_editor.save.failed": "This article could not be saved",
  44 + "article.basic_editor.cancel": "Cancel",
  45 + "article.basic_editor.success.title": "Good job!",
  46 + "article.basic_editor.success.message": "Article saved!",
  47 + "article.basic_editor.visibility": "Visibility",
  48 + "article.basic_editor.visibility.public": "Public",
  49 + "article.basic_editor.visibility.private": "Private"
39 } 50 }
src/languages/pt.json
@@ -35,5 +35,16 @@ @@ -35,5 +35,16 @@
35 "comment.pagination.more": "Mais", 35 "comment.pagination.more": "Mais",
36 "comment.post.success.title": "Bom trabalho!", 36 "comment.post.success.title": "Bom trabalho!",
37 "comment.post.success.message": "Comentário salvo com sucesso!", 37 "comment.post.success.message": "Comentário salvo com sucesso!",
38 - "comment.reply": "responder" 38 + "comment.reply": "responder",
  39 + "article.actions.edit": "Editar",
  40 + "article.basic_editor.title": "Título",
  41 + "article.basic_editor.body": "Corpo",
  42 + "article.basic_editor.save": "Salvar",
  43 + "article.basic_editor.save.failed": "O artigo não pode ser salvo",
  44 + "article.basic_editor.cancel": "Cancelar",
  45 + "article.basic_editor.success.title": "Bom trabalho!",
  46 + "article.basic_editor.success.message": "Artigo salvo com sucesso!",
  47 + "article.basic_editor.visibility": "Visibilidade",
  48 + "article.basic_editor.visibility.public": "Público",
  49 + "article.basic_editor.visibility.private": "Privado"
39 } 50 }
src/lib/ng-noosfero-api/http/article.service.ts
@@ -22,6 +22,17 @@ export class ArticleService extends RestangularService&lt;noosfero.Article&gt; { @@ -22,6 +22,17 @@ export class ArticleService extends RestangularService&lt;noosfero.Article&gt; {
22 }; 22 };
23 } 23 }
24 24
  25 + updateArticle(article: noosfero.Article) {
  26 + let headers = {
  27 + 'Content-Type': 'application/json'
  28 + };
  29 + let deferred = this.$q.defer<noosfero.RestResult<noosfero.Article>>();
  30 + let attributesToUpdate: any = { article: { name: article.name, body: article.body, published: article.published } };
  31 + let restRequest: ng.IPromise<noosfero.RestResult<noosfero.Article>> = this.getElement(article.id).customPOST(attributesToUpdate, null, null, headers);
  32 + restRequest.then(this.getHandleSuccessFunction(deferred))
  33 + .catch(this.getHandleErrorFunction(deferred));
  34 + return deferred.promise;
  35 + }
25 36
26 createInProfile(profile: noosfero.Profile, article: noosfero.Article): ng.IPromise<noosfero.RestResult<noosfero.Article>> { 37 createInProfile(profile: noosfero.Profile, article: noosfero.Article): ng.IPromise<noosfero.RestResult<noosfero.Article>> {
27 let profileElement = this.profileService.get(<number>profile.id); 38 let profileElement = this.profileService.get(<number>profile.id);
@@ -32,6 +43,14 @@ export class ArticleService extends RestangularService&lt;noosfero.Article&gt; { @@ -32,6 +43,14 @@ export class ArticleService extends RestangularService&lt;noosfero.Article&gt; {
32 return this.create(article, <noosfero.RestModel>profileElement, null, headers); 43 return this.create(article, <noosfero.RestModel>profileElement, null, headers);
33 } 44 }
34 45
  46 + createInParent(parentId: number, article: noosfero.Article): ng.IPromise<noosfero.RestResult<noosfero.Article>> {
  47 + let headers = {
  48 + 'Content-Type': 'application/json'
  49 + };
  50 +
  51 + let parent = this.getElement(parentId);
  52 + return this.create(article, parent, null, headers, true, "children");
  53 + }
35 54
36 getAsCollectionChildrenOf<C>(rootElement: noosfero.Environment | noosfero.Article | noosfero.Profile, path: string, queryParams?: any, headers?: any): restangular.ICollectionPromise<C> { 55 getAsCollectionChildrenOf<C>(rootElement: noosfero.Environment | noosfero.Article | noosfero.Profile, path: string, queryParams?: any, headers?: any): restangular.ICollectionPromise<C> {
37 return rootElement.getList<C>(path, queryParams, headers); 56 return rootElement.getList<C>(path, queryParams, headers);
@@ -77,5 +96,4 @@ export class ArticleService extends RestangularService&lt;noosfero.Article&gt; { @@ -77,5 +96,4 @@ export class ArticleService extends RestangularService&lt;noosfero.Article&gt; {
77 } 96 }
78 97
79 98
80 -  
81 } 99 }
src/lib/ng-noosfero-api/http/restangular_service.ts
@@ -11,6 +11,8 @@ @@ -11,6 +11,8 @@
11 export abstract class RestangularService<T extends noosfero.RestModel> { 11 export abstract class RestangularService<T extends noosfero.RestModel> {
12 12
13 private baseResource: restangular.IElement; 13 private baseResource: restangular.IElement;
  14 + private currentPromise: ng.IDeferred<T>;
  15 +
14 /** 16 /**
15 * Creates an instance of RestangularService. 17 * Creates an instance of RestangularService.
16 * 18 *
@@ -20,6 +22,7 @@ export abstract class RestangularService&lt;T extends noosfero.RestModel&gt; { @@ -20,6 +22,7 @@ export abstract class RestangularService&lt;T extends noosfero.RestModel&gt; {
20 */ 22 */
21 constructor(protected restangularService: restangular.IService, protected $q: ng.IQService, protected $log: ng.ILogService) { 23 constructor(protected restangularService: restangular.IService, protected $q: ng.IQService, protected $log: ng.ILogService) {
22 this.baseResource = restangularService.all(this.getResourcePath()); 24 this.baseResource = restangularService.all(this.getResourcePath());
  25 + this.resetCurrent();
23 // TODO 26 // TODO
24 // this.restangularService.setResponseInterceptor((data, operation, what, url, response, deferred) => { 27 // this.restangularService.setResponseInterceptor((data, operation, what, url, response, deferred) => {
25 // let transformedData: any = data; 28 // let transformedData: any = data;
@@ -32,6 +35,18 @@ export abstract class RestangularService&lt;T extends noosfero.RestModel&gt; { @@ -32,6 +35,18 @@ export abstract class RestangularService&lt;T extends noosfero.RestModel&gt; {
32 // }); 35 // });
33 } 36 }
34 37
  38 + public resetCurrent() {
  39 + this.currentPromise = this.$q.defer();
  40 + }
  41 +
  42 + public getCurrent(): ng.IPromise<T> {
  43 + return this.currentPromise.promise;
  44 + }
  45 +
  46 + public setCurrent(object: T) {
  47 + this.currentPromise.resolve(object);
  48 + }
  49 +
35 protected extractData(response: restangular.IResponse): noosfero.RestResult<T> { 50 protected extractData(response: restangular.IResponse): noosfero.RestResult<T> {
36 let dataKey: string; 51 let dataKey: string;
37 if (response.data && this.getDataKeys()) { 52 if (response.data && this.getDataKeys()) {
@@ -221,7 +236,7 @@ export abstract class RestangularService&lt;T extends noosfero.RestModel&gt; { @@ -221,7 +236,7 @@ export abstract class RestangularService&lt;T extends noosfero.RestModel&gt; {
221 * Creates a new Resource into the resource collection 236 * Creates a new Resource into the resource collection
222 * calls POST /resourcePath 237 * calls POST /resourcePath
223 */ 238 */
224 - public create(obj: T, rootElement?: noosfero.RestModel, queryParams?: any, headers?: any, isSub: boolean = true): ng.IPromise<noosfero.RestResult<T>> { 239 + public create(obj: T, rootElement?: noosfero.RestModel, queryParams?: any, headers?: any, isSub: boolean = true, path?: string): ng.IPromise<noosfero.RestResult<T>> {
225 let deferred = this.$q.defer<noosfero.RestResult<T>>(); 240 let deferred = this.$q.defer<noosfero.RestResult<T>>();
226 241
227 let restRequest: ng.IPromise<noosfero.RestResult<T>>; 242 let restRequest: ng.IPromise<noosfero.RestResult<T>>;
@@ -233,8 +248,9 @@ export abstract class RestangularService&lt;T extends noosfero.RestModel&gt; { @@ -233,8 +248,9 @@ export abstract class RestangularService&lt;T extends noosfero.RestModel&gt; {
233 data = obj; 248 data = obj;
234 } 249 }
235 250
  251 + let subpath = path || this.getResourcePath();
236 if (rootElement) { 252 if (rootElement) {
237 - restRequest = rootElement.all(this.getResourcePath()).post(data, queryParams, headers); 253 + restRequest = rootElement.all(subpath).post(data, queryParams, headers);
238 } else { 254 } else {
239 restRequest = this.baseResource.post(data, queryParams, headers); 255 restRequest = this.baseResource.post(data, queryParams, headers);
240 } 256 }
src/lib/ng-noosfero-api/interfaces/article.ts
1 1
2 namespace noosfero { 2 namespace noosfero {
3 - export interface Article extends RestModel { 3 + export interface Article extends RestModel {
4 path: string; 4 path: string;
5 profile: Profile; 5 profile: Profile;
6 type: string; 6 type: string;
  7 + parent: Article;
  8 + body: string;
  9 + title: string;
  10 + name: string;
  11 + published: boolean;
7 } 12 }
8 -}  
9 \ No newline at end of file 13 \ No newline at end of file
  14 +}
src/spec/mocks.ts
1 const DEBUG = false; 1 const DEBUG = false;
2 2
3 -let log = (message: string, ...args: any[]) => { 3 +let log = (message: string, ...args: any[]) => {
4 if (DEBUG) { 4 if (DEBUG) {
5 console.log(message); 5 console.log(message);
6 } 6 }
@@ -70,7 +70,9 @@ export var mocks = { @@ -70,7 +70,9 @@ export var mocks = {
70 return { 70 return {
71 then: (func?: Function) => { if (func) func(); } 71 then: (func?: Function) => { if (func) func(); }
72 }; 72 };
73 - } 73 + },
  74 + setCurrent: (article: noosfero.Article) => { },
  75 + getCurrent: () => { return Promise.resolve({}); }
74 }, 76 },
75 environmentService: { 77 environmentService: {
76 getEnvironmentPeople: (params: any) => { 78 getEnvironmentPeople: (params: any) => {