Commit 1894afc0521954895e6b817afa85c8c4a51e8d5e
Exists in
master
and in
27 other branches
Merge branch 'create-article' into 'master'
Basic form to create and edit articles See merge request !11
Showing
27 changed files
with
406 additions
and
164 deletions
Show diff stats
bower.json
... | ... | @@ -34,7 +34,9 @@ |
34 | 34 | "angular-dynamic-locale": "^0.1.30", |
35 | 35 | "angular-i18n": "^1.5.0", |
36 | 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 | 41 | "devDependencies": { |
40 | 42 | "angular-mocks": "~1.5.0" | ... | ... |
src/app/article/article.html
... | ... | @@ -4,6 +4,9 @@ |
4 | 4 | </div> |
5 | 5 | |
6 | 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 | 10 | <div class="page-info pull-right small text-muted"> |
8 | 11 | <span class="time"> |
9 | 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 | -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 | -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 | -<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 @@ |
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 | +}); | ... | ... |
... | ... | @@ -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 | +} | ... | ... |
... | ... | @@ -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> | ... | ... |
... | ... | @@ -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 | 1 | import {Input, Component, provide} from 'ng-forward'; |
4 | 2 | |
5 | 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 | 5 | import {ContentViewerActionsComponent} from './content-viewer-actions.component'; |
9 | 6 | |
10 | 7 | // this htmlTemplate will be re-used between the container components in this spec file |
... | ... | @@ -12,56 +9,57 @@ const htmlTemplate: string = '<content-viewer-actions [article]="ctrl.article" [ |
12 | 9 | |
13 | 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 | 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 | 54 | let profile: any = { |
49 | 55 | id: 1, |
50 | 56 | identifier: 'the-profile-test', |
51 | 57 | type: 'Person' |
52 | 58 | }; |
53 | - | |
54 | 59 | helpers.mocks.profileService.getCurrentProfile = () => { |
55 | 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 | 1 | import {Component, Inject, provide} from "ng-forward"; |
2 | 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 | 5 | @Component({ |
5 | 6 | selector: "content-viewer-actions", |
6 | 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 | 14 | export class ContentViewerActionsComponent { |
11 | 15 | |
12 | 16 | article: noosfero.Article; |
13 | 17 | profile: noosfero.Profile; |
18 | + parentId: number; | |
14 | 19 | |
15 | - constructor(profileService: ProfileService) { | |
20 | + constructor(profileService: ProfileService, articleService: ArticleService) { | |
16 | 21 | profileService.getCurrentProfile().then((profile: noosfero.Profile) => { |
17 | 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 | 28 | } |
29 | 29 | |
30 | 30 | activate() { |
31 | - this.profileService.getCurrentProfile().then((profile: noosfero.Profile) => { | |
31 | + this.profileService.getCurrentProfile().then((profile: noosfero.Profile) => { | |
32 | 32 | this.profile = profile; |
33 | 33 | return this.articleService.getArticleByProfileAndPath(this.profile, this.$stateParams["page"]); |
34 | 34 | }).then((result: noosfero.RestResult<any>) => { |
35 | 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 | 1 | <ul class="nav navbar-nav"> |
2 | 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 | 4 | <i class="fa fa-file fa-fw fa-lg"></i> {{"navbar.content_viewer_actions.new_post" | translate}} |
5 | 5 | </a> |
6 | 6 | </li> | ... | ... |
src/app/article/index.ts
src/app/index.ts
... | ... | @@ -14,7 +14,7 @@ declare var moment: any; |
14 | 14 | |
15 | 15 | let noosferoApp: any = bundle("noosferoApp", MainComponent, ["ngAnimate", "ngCookies", "ngStorage", "ngTouch", |
16 | 16 | "ngSanitize", "ngMessages", "ngAria", "restangular", |
17 | - "ui.router", "ui.bootstrap", "toastr", | |
17 | + "ui.router", "ui.bootstrap", "toastr", "ngCkeditor", | |
18 | 18 | "angularMoment", "angular.filter", "akoenig.deckgrid", |
19 | 19 | "angular-timeline", "duScroll", "oitozero.ngSweetAlert", |
20 | 20 | "pascalprecht.translate", "tmh.dynamicLocale", "angularLoad"]).publish(); | ... | ... |
src/app/main/main.component.ts
... | ... | @@ -29,6 +29,7 @@ import {BodyStateClassesService} from "./../layout/services/body-state-classes.s |
29 | 29 | import {Navbar} from "../layout/navbar/navbar"; |
30 | 30 | |
31 | 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 | 83 | directives: [ |
83 | 84 | ArticleBlogComponent, ArticleViewComponent, BoxesComponent, BlockComponent, |
84 | 85 | EnvironmentComponent, PeopleBlockComponent, |
85 | - LinkListBlockComponent, CommunitiesBlockComponent, | |
86 | + LinkListBlockComponent, CommunitiesBlockComponent, HtmlEditorComponent, | |
86 | 87 | MainBlockComponent, RecentDocumentsBlockComponent, Navbar, ProfileImageBlockComponent, |
87 | 88 | MembersBlockComponent, NoosferoTemplate, DateFormat, RawHTMLBlockComponent |
88 | 89 | ], | ... | ... |
src/app/profile/profile.component.ts
1 | 1 | import {StateConfig, Component, Inject, provide} from 'ng-forward'; |
2 | 2 | import {ProfileInfoComponent} from './info/profile-info.component'; |
3 | 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 | 5 | import {ContentViewerComponent} from "../article/content-viewer/content-viewer.component"; |
6 | 6 | import {ContentViewerActionsComponent} from "../article/content-viewer/content-viewer-actions.component"; |
7 | 7 | import {ActivitiesComponent} from "./activities/activities.component"; |
... | ... | @@ -45,12 +45,24 @@ import {MyProfileComponent} from "./myprofile.component"; |
45 | 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 | 50 | component: BasicEditorComponent, |
51 | 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 | 66 | controller: BasicEditorComponent, |
55 | 67 | controllerAs: "vm" |
56 | 68 | } |
... | ... | @@ -98,7 +110,7 @@ export class ProfileComponent { |
98 | 110 | }).then((response: restangular.IResponse) => { |
99 | 111 | this.boxes = response.data.boxes; |
100 | 112 | }).catch(() => { |
101 | - $state.transitionTo('main'); | |
113 | + $state.transitionTo('main.environment.home'); | |
102 | 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 @@ |
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 @@ |
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 | +} | ... | ... |
... | ... | @@ -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 | 25 | |
26 | 26 | httpError(status: number, data: any): boolean { |
27 | 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 | 31 | success({ | ... | ... |
src/languages/en.json
... | ... | @@ -35,5 +35,16 @@ |
35 | 35 | "comment.pagination.more": "More", |
36 | 36 | "comment.post.success.title": "Good job!", |
37 | 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 | 35 | "comment.pagination.more": "Mais", |
36 | 36 | "comment.post.success.title": "Bom trabalho!", |
37 | 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<noosfero.Article> { |
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 | 37 | createInProfile(profile: noosfero.Profile, article: noosfero.Article): ng.IPromise<noosfero.RestResult<noosfero.Article>> { |
27 | 38 | let profileElement = this.profileService.get(<number>profile.id); |
... | ... | @@ -32,6 +43,14 @@ export class ArticleService extends RestangularService<noosfero.Article> { |
32 | 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 | 55 | getAsCollectionChildrenOf<C>(rootElement: noosfero.Environment | noosfero.Article | noosfero.Profile, path: string, queryParams?: any, headers?: any): restangular.ICollectionPromise<C> { |
37 | 56 | return rootElement.getList<C>(path, queryParams, headers); |
... | ... | @@ -77,5 +96,4 @@ export class ArticleService extends RestangularService<noosfero.Article> { |
77 | 96 | } |
78 | 97 | |
79 | 98 | |
80 | - | |
81 | 99 | } | ... | ... |
src/lib/ng-noosfero-api/http/restangular_service.ts
... | ... | @@ -11,6 +11,8 @@ |
11 | 11 | export abstract class RestangularService<T extends noosfero.RestModel> { |
12 | 12 | |
13 | 13 | private baseResource: restangular.IElement; |
14 | + private currentPromise: ng.IDeferred<T>; | |
15 | + | |
14 | 16 | /** |
15 | 17 | * Creates an instance of RestangularService. |
16 | 18 | * |
... | ... | @@ -20,6 +22,7 @@ export abstract class RestangularService<T extends noosfero.RestModel> { |
20 | 22 | */ |
21 | 23 | constructor(protected restangularService: restangular.IService, protected $q: ng.IQService, protected $log: ng.ILogService) { |
22 | 24 | this.baseResource = restangularService.all(this.getResourcePath()); |
25 | + this.resetCurrent(); | |
23 | 26 | // TODO |
24 | 27 | // this.restangularService.setResponseInterceptor((data, operation, what, url, response, deferred) => { |
25 | 28 | // let transformedData: any = data; |
... | ... | @@ -32,6 +35,18 @@ export abstract class RestangularService<T extends noosfero.RestModel> { |
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 | 50 | protected extractData(response: restangular.IResponse): noosfero.RestResult<T> { |
36 | 51 | let dataKey: string; |
37 | 52 | if (response.data && this.getDataKeys()) { |
... | ... | @@ -221,7 +236,7 @@ export abstract class RestangularService<T extends noosfero.RestModel> { |
221 | 236 | * Creates a new Resource into the resource collection |
222 | 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 | 240 | let deferred = this.$q.defer<noosfero.RestResult<T>>(); |
226 | 241 | |
227 | 242 | let restRequest: ng.IPromise<noosfero.RestResult<T>>; |
... | ... | @@ -233,8 +248,9 @@ export abstract class RestangularService<T extends noosfero.RestModel> { |
233 | 248 | data = obj; |
234 | 249 | } |
235 | 250 | |
251 | + let subpath = path || this.getResourcePath(); | |
236 | 252 | if (rootElement) { |
237 | - restRequest = rootElement.all(this.getResourcePath()).post(data, queryParams, headers); | |
253 | + restRequest = rootElement.all(subpath).post(data, queryParams, headers); | |
238 | 254 | } else { |
239 | 255 | restRequest = this.baseResource.post(data, queryParams, headers); |
240 | 256 | } | ... | ... |
src/lib/ng-noosfero-api/interfaces/article.ts
1 | 1 | |
2 | 2 | namespace noosfero { |
3 | - export interface Article extends RestModel { | |
3 | + export interface Article extends RestModel { | |
4 | 4 | path: string; |
5 | 5 | profile: Profile; |
6 | 6 | type: string; |
7 | + parent: Article; | |
8 | + body: string; | |
9 | + title: string; | |
10 | + name: string; | |
11 | + published: boolean; | |
7 | 12 | } |
8 | -} | |
9 | 13 | \ No newline at end of file |
14 | +} | ... | ... |
src/spec/mocks.ts
1 | 1 | const DEBUG = false; |
2 | 2 | |
3 | -let log = (message: string, ...args: any[]) => { | |
3 | +let log = (message: string, ...args: any[]) => { | |
4 | 4 | if (DEBUG) { |
5 | 5 | console.log(message); |
6 | 6 | } |
... | ... | @@ -70,7 +70,9 @@ export var mocks = { |
70 | 70 | return { |
71 | 71 | then: (func?: Function) => { if (func) func(); } |
72 | 72 | }; |
73 | - } | |
73 | + }, | |
74 | + setCurrent: (article: noosfero.Article) => { }, | |
75 | + getCurrent: () => { return Promise.resolve({}); } | |
74 | 76 | }, |
75 | 77 | environmentService: { |
76 | 78 | getEnvironmentPeople: (params: any) => { | ... | ... |