Commit 69ff070c1cd62386ad769d7400c3436e37a6d7d9

Authored by Victor Costa
1 parent aa54e8a5

Component to search for articles in the environment

src/app/environment/environment.component.ts
@@ -2,6 +2,7 @@ import {StateConfig, Component, Inject, provide} from 'ng-forward'; @@ -2,6 +2,7 @@ import {StateConfig, Component, Inject, provide} from 'ng-forward';
2 import {EnvironmentService} from "../../lib/ng-noosfero-api/http/environment.service"; 2 import {EnvironmentService} from "../../lib/ng-noosfero-api/http/environment.service";
3 import {NotificationService} from "../shared/services/notification.service"; 3 import {NotificationService} from "../shared/services/notification.service";
4 import {EnvironmentHomeComponent} from "./environment-home.component"; 4 import {EnvironmentHomeComponent} from "./environment-home.component";
  5 +import {SearchComponent} from "../search/search.component";
5 6
6 /** 7 /**
7 * @ngdoc controller 8 * @ngdoc controller
@@ -29,6 +30,18 @@ import {EnvironmentHomeComponent} from "./environment-home.component"; @@ -29,6 +30,18 @@ import {EnvironmentHomeComponent} from "./environment-home.component";
29 controllerAs: "vm" 30 controllerAs: "vm"
30 } 31 }
31 } 32 }
  33 + },
  34 + {
  35 + url: '^/search?query',
  36 + component: SearchComponent,
  37 + name: 'main.environment.search',
  38 + views: {
  39 + "mainBlockContent": {
  40 + templateUrl: "app/search/search.html",
  41 + controller: SearchComponent,
  42 + controllerAs: "ctrl"
  43 + }
  44 + }
32 } 45 }
33 ]) 46 ])
34 @Inject(EnvironmentService, "$state", "currentEnvironment") 47 @Inject(EnvironmentService, "$state", "currentEnvironment")
src/app/layout/navbar/navbar.html
@@ -43,6 +43,9 @@ @@ -43,6 +43,9 @@
43 <language-selector class="nav navbar-nav navbar-right"></language-selector> 43 <language-selector class="nav navbar-nav navbar-right"></language-selector>
44 </ul> 44 </ul>
45 <div ui-view="actions"></div> 45 <div ui-view="actions"></div>
  46 + <div class="nav navbar-nav search navbar-right">
  47 + <search-form></search-form>
  48 + </div>
46 </div> 49 </div>
47 </div> 50 </div>
48 </nav> 51 </nav>
src/app/layout/navbar/navbar.scss
@@ -34,6 +34,16 @@ @@ -34,6 +34,16 @@
34 text-align: center; 34 text-align: center;
35 } 35 }
36 36
  37 + .search {
  38 + .search-input {
  39 + height: 45px;
  40 + margin-top: 10px;
  41 + }
  42 + .input-group-btn {
  43 + padding-top: 10px;
  44 + }
  45 + }
  46 +
37 @media (max-width: $break-sm-max) { 47 @media (max-width: $break-sm-max) {
38 .navbar-toggle .fa-bars{ 48 .navbar-toggle .fa-bars{
39 font-size: 14pt; 49 font-size: 14pt;
src/app/layout/scss/skins/_whbl.scss
@@ -180,27 +180,19 @@ $whbl-font-color: #16191c; @@ -180,27 +180,19 @@ $whbl-font-color: #16191c;
180 } 180 }
181 .pagination { 181 .pagination {
182 > li { 182 > li {
183 - > a,  
184 - > span,  
185 - > a:hover,  
186 - > span:hover,  
187 - > a:focus,  
188 - > span:focus,  
189 - > a:active,  
190 - > span:active {  
191 - color: $whbl-primary-color; 183 + &.active {
  184 + a {
  185 + background-color: $whbl-primary-color;
  186 + color: #FFF;
  187 + }
192 } 188 }
193 - }  
194 - > .active {  
195 - > a,  
196 - > span,  
197 - > a:hover,  
198 - > span:hover,  
199 - > a:focus,  
200 - > span:focus {  
201 - background-color: $whbl-primary-color;  
202 - border-color: $whbl-primary-color;  
203 - color: #fff; 189 + > a {
  190 + &:hover {
  191 + background-color: $whbl-primary-color;
  192 + color: #FFF;
  193 + }
  194 + background-color: #FFF;
  195 + color: $whbl-primary-color;
204 } 196 }
205 } 197 }
206 } 198 }
src/app/main/main.component.ts
@@ -40,6 +40,8 @@ import {SidebarComponent} from &quot;../layout/sidebar/sidebar.component&quot;; @@ -40,6 +40,8 @@ import {SidebarComponent} from &quot;../layout/sidebar/sidebar.component&quot;;
40 import {MainBlockComponent} from "../layout/blocks/main/main-block.component"; 40 import {MainBlockComponent} from "../layout/blocks/main/main-block.component";
41 import {HtmlEditorComponent} from "../shared/components/html-editor/html-editor.component"; 41 import {HtmlEditorComponent} from "../shared/components/html-editor/html-editor.component";
42 import {PermissionDirective} from "../shared/components/permission/permission.directive"; 42 import {PermissionDirective} from "../shared/components/permission/permission.directive";
  43 +import {SearchComponent} from "../search/search.component";
  44 +import {SearchFormComponent} from "../search/search-form/search-form.component";
43 45
44 /** 46 /**
45 * @ngdoc controller 47 * @ngdoc controller
@@ -100,7 +102,7 @@ export class EnvironmentContent { @@ -100,7 +102,7 @@ export class EnvironmentContent {
100 LinkListBlockComponent, CommunitiesBlockComponent, HtmlEditorComponent, ProfileComponent, 102 LinkListBlockComponent, CommunitiesBlockComponent, HtmlEditorComponent, ProfileComponent,
101 MainBlockComponent, RecentDocumentsBlockComponent, Navbar, SidebarComponent, ProfileImageBlockComponent, 103 MainBlockComponent, RecentDocumentsBlockComponent, Navbar, SidebarComponent, ProfileImageBlockComponent,
102 MembersBlockComponent, NoosferoTemplate, DateFormat, RawHTMLBlockComponent, StatisticsBlockComponent, 104 MembersBlockComponent, NoosferoTemplate, DateFormat, RawHTMLBlockComponent, StatisticsBlockComponent,
103 - LoginBlockComponent, CustomContentComponent, PermissionDirective 105 + LoginBlockComponent, CustomContentComponent, PermissionDirective, SearchFormComponent, SearchComponent
104 ].concat(plugins.mainComponents).concat(plugins.hotspots), 106 ].concat(plugins.mainComponents).concat(plugins.hotspots),
105 providers: [AuthService, SessionService, NotificationService, BodyStateClassesService, 107 providers: [AuthService, SessionService, NotificationService, BodyStateClassesService,
106 "ngAnimate", "ngCookies", "ngStorage", "ngTouch", 108 "ngAnimate", "ngCookies", "ngStorage", "ngTouch",
src/app/search/search-form/search-form.component.spec.ts 0 → 100644
@@ -0,0 +1,34 @@ @@ -0,0 +1,34 @@
  1 +import {ComponentTestHelper, createClass} from "../../../spec/component-test-helper";
  2 +import {SearchFormComponent} from "./search-form.component";
  3 +import * as helpers from "../../../spec/helpers";
  4 +
  5 +const htmlTemplate: string = '<search-form></search-form>';
  6 +
  7 +describe("Components", () => {
  8 + describe("Search Form Component", () => {
  9 +
  10 + let helper: ComponentTestHelper<SearchFormComponent>;
  11 + let stateMock = jasmine.createSpyObj("$state", ["go"]);
  12 +
  13 + beforeEach(angular.mock.module("templates"));
  14 +
  15 + beforeEach((done) => {
  16 + let cls = createClass({
  17 + template: htmlTemplate,
  18 + directives: [SearchFormComponent],
  19 + providers: [helpers.createProviderToValue("$state", stateMock)]
  20 + });
  21 + helper = new ComponentTestHelper<SearchFormComponent>(cls, done);
  22 + });
  23 +
  24 + it("render a input for search query", () => {
  25 + expect(helper.find(".search-input").length).toEqual(1);
  26 + });
  27 +
  28 + it("go to search page when click on search button", () => {
  29 + helper.component.query = 'query';
  30 + helper.component.search();
  31 + expect(stateMock.go).toHaveBeenCalledWith('main.environment.search', { query: 'query' });
  32 + });
  33 + });
  34 +});
src/app/search/search-form/search-form.component.ts 0 → 100644
@@ -0,0 +1,17 @@ @@ -0,0 +1,17 @@
  1 +import {Component, Inject} from "ng-forward";
  2 +
  3 +@Component({
  4 + selector: 'search-form',
  5 + templateUrl: 'app/search/search-form/search-form.html'
  6 +})
  7 +@Inject("$state")
  8 +export class SearchFormComponent {
  9 +
  10 + query: string;
  11 +
  12 + constructor(private $state: ng.ui.IStateService) { }
  13 +
  14 + search() {
  15 + this.$state.go('main.environment.search', { query: this.query });
  16 + }
  17 +}
src/app/search/search-form/search-form.html 0 → 100644
@@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
  1 +<form class="navbar-form search-form" role="search">
  2 + <div class="input-group">
  3 + <input type="text" class="search-input form-control" placeholder="Search" name="q" ng-model="ctrl.query">
  4 + <div class="input-group-btn">
  5 + <button class="btn btn-default" type="submit" (click)="ctrl.search()"><i class="fa fa-search fa-fw"></i></button>
  6 + </div>
  7 + </div>
  8 +</form>
src/app/search/search.component.spec.ts 0 → 100644
@@ -0,0 +1,38 @@ @@ -0,0 +1,38 @@
  1 +import {ComponentTestHelper, createClass} from "../../spec/component-test-helper";
  2 +import {SearchComponent} from "./search.component";
  3 +import * as helpers from "../../spec/helpers";
  4 +
  5 +const htmlTemplate: string = '<search></search>';
  6 +
  7 +describe("Components", () => {
  8 + describe("Search Component", () => {
  9 +
  10 + let helper: ComponentTestHelper<SearchComponent>;
  11 + let stateParams = { query: 'query' };
  12 + let articleService = jasmine.createSpyObj("ArticleService", ["search"]);
  13 + let result = Promise.resolve({ data: [{ id: 1 }], headers: (param: string) => { return 1; } });
  14 + articleService.search = jasmine.createSpy("search").and.returnValue(result);
  15 +
  16 + beforeEach(angular.mock.module("templates"));
  17 +
  18 + beforeEach((done) => {
  19 + let cls = createClass({
  20 + template: htmlTemplate,
  21 + directives: [SearchComponent],
  22 + providers: [
  23 + helpers.createProviderToValue("$stateParams", stateParams),
  24 + helpers.createProviderToValue("ArticleService", articleService)
  25 + ].concat(helpers.provideFilters("truncateFilter", "stripTagsFilter"))
  26 + });
  27 + helper = new ComponentTestHelper<SearchComponent>(cls, done);
  28 + });
  29 +
  30 + it("load first page with search results", () => {
  31 + expect(articleService.search).toHaveBeenCalledWith({ query: 'query', per_page: 10, page: 0 });
  32 + });
  33 +
  34 + it("display search results", () => {
  35 + expect(helper.all(".result").length).toEqual(1);
  36 + });
  37 + });
  38 +});
src/app/search/search.component.ts 0 → 100644
@@ -0,0 +1,33 @@ @@ -0,0 +1,33 @@
  1 +import {Component, Inject} from "ng-forward";
  2 +import {ArticleService} from "./../../lib/ng-noosfero-api/http/article.service";
  3 +
  4 +@Component({
  5 + selector: 'search',
  6 + templateUrl: 'app/search/search.html'
  7 +})
  8 +@Inject(ArticleService, "$stateParams")
  9 +export class SearchComponent {
  10 +
  11 + articles: noosfero.Article[];
  12 + query: string;
  13 + totalResults = 0;
  14 + perPage = 10;
  15 + currentPage: number = 0;
  16 +
  17 + constructor(private articleService: ArticleService, private $stateParams: ng.ui.IStateParamsService) {
  18 + this.query = this.$stateParams['query'];
  19 + this.loadPage();
  20 + }
  21 +
  22 + loadPage() {
  23 + let filters = {
  24 + query: this.query,
  25 + per_page: this.perPage,
  26 + page: this.currentPage
  27 + };
  28 + this.articleService.search(filters).then((result: noosfero.RestResult<noosfero.Article[]>) => {
  29 + this.totalResults = <number>result.headers("total");
  30 + this.articles = result.data;
  31 + });
  32 + }
  33 +}
src/app/search/search.html 0 → 100644
@@ -0,0 +1,26 @@ @@ -0,0 +1,26 @@
  1 +<h2>Search</h2>
  2 +
  3 +<div class="search-results">
  4 + <div class="summary">
  5 + {{"search.results.summary" | translate:{results: ctrl.totalResults}:"messageformat"}}
  6 + </div>
  7 + <div ng-repeat="article in ctrl.articles | orderBy: 'created_at':true" class="result">
  8 + <a class="title" ui-sref="main.profile.page({profile: article.profile.identifier, page: article.path})">
  9 + <h4 ng-bind="article.title"></h4>
  10 + </a>
  11 + <div class="info">
  12 + <a class="profile" ui-sref="main.profile.home({profile: article.profile.identifier})">
  13 + {{article.profile.name}}
  14 + </a>
  15 + <span class="bullet-separator">•</span>
  16 + <span class="time">
  17 + <span am-time-ago="article.created_at | dateFormat"></span>
  18 + </span>
  19 + </div>
  20 + <div class="post-lead" ng-bind-html="article.body | stripTags | truncate: 250: '...': true"></div>
  21 + </div>
  22 + <uib-pagination ng-model="ctrl.currentPage" total-items="ctrl.totalResults" class="pagination-sm center-block"
  23 + boundary-links="true" items-per-page="ctrl.perPage" ng-change="ctrl.loadPage()"
  24 + first-text="«" last-text="»" previous-text="‹" next-text="›">
  25 + </uib-pagination>
  26 +</div>
src/app/search/search.scss 0 → 100644
@@ -0,0 +1,30 @@ @@ -0,0 +1,30 @@
  1 +.search-results {
  2 + .summary {
  3 + color: #bbbbbb;
  4 + font-size: 13px;
  5 + border-top: 1px solid #ececec;
  6 + }
  7 + .result {
  8 + margin: 25px 0;
  9 +
  10 + .title {
  11 + h4 {
  12 + margin: 0;
  13 + }
  14 + }
  15 + .info {
  16 + .profile {
  17 + color: #6e9e7b;
  18 + }
  19 + .time {
  20 + color: #6e9e7b;
  21 + font-size: 12px;
  22 + }
  23 + .bullet-separator {
  24 + margin: 0 2px;
  25 + font-size: 10px;
  26 + color: #afd6ba;
  27 + }
  28 + }
  29 + }
  30 +}
src/languages/en.json
@@ -74,5 +74,6 @@ @@ -74,5 +74,6 @@
74 "profile.content.success.message": "Profile saved!", 74 "profile.content.success.message": "Profile saved!",
75 "custom_content.title": "Edit content", 75 "custom_content.title": "Edit content",
76 "profile.custom_header.label": "Header", 76 "profile.custom_header.label": "Header",
77 - "profile.custom_footer.label": "Footer" 77 + "profile.custom_footer.label": "Footer",
  78 + "search.results.summary": "{results, plural, one{result} other{# results}}"
78 } 79 }
src/languages/pt.json
@@ -74,5 +74,6 @@ @@ -74,5 +74,6 @@
74 "profile.content.success.message": "Perfil salvo!", 74 "profile.content.success.message": "Perfil salvo!",
75 "custom_content.title": "Editar conteúdo", 75 "custom_content.title": "Editar conteúdo",
76 "profile.custom_header.label": "Cabeçalho", 76 "profile.custom_header.label": "Cabeçalho",
77 - "profile.custom_footer.label": "Rodapé" 77 + "profile.custom_footer.label": "Rodapé",
  78 + "search.results.summary": "{results, plural, one{result} other{# resultados}}"
78 } 79 }
src/lib/ng-noosfero-api/http/article.service.spec.ts
@@ -23,7 +23,7 @@ describe(&quot;Services&quot;, () =&gt; { @@ -23,7 +23,7 @@ describe(&quot;Services&quot;, () =&gt; {
23 it("should remove article", (done) => { 23 it("should remove article", (done) => {
24 let articleId = 1; 24 let articleId = 1;
25 $httpBackend.expectDELETE(`/api/v1/articles/${articleId}`).respond(200, { success: "true" }); 25 $httpBackend.expectDELETE(`/api/v1/articles/${articleId}`).respond(200, { success: "true" });
26 - articleService.remove(<noosfero.Article>{id: articleId}); 26 + articleService.remove(<noosfero.Article>{ id: articleId });
27 $httpBackend.flush(); 27 $httpBackend.flush();
28 $httpBackend.verifyNoOutstandingExpectation(); 28 $httpBackend.verifyNoOutstandingExpectation();
29 done(); 29 done();
@@ -32,7 +32,7 @@ describe(&quot;Services&quot;, () =&gt; { @@ -32,7 +32,7 @@ describe(&quot;Services&quot;, () =&gt; {
32 it("should return article children", (done) => { 32 it("should return article children", (done) => {
33 let articleId = 1; 33 let articleId = 1;
34 $httpBackend.expectGET(`/api/v1/articles/${articleId}/children`).respond(200, { articles: [{ name: "article1" }] }); 34 $httpBackend.expectGET(`/api/v1/articles/${articleId}/children`).respond(200, { articles: [{ name: "article1" }] });
35 - articleService.getChildren(<noosfero.Article>{id: articleId}).then((result: noosfero.RestResult<noosfero.Article[]>) => { 35 + articleService.getChildren(<noosfero.Article>{ id: articleId }).then((result: noosfero.RestResult<noosfero.Article[]>) => {
36 expect(result.data).toEqual([{ name: "article1" }]); 36 expect(result.data).toEqual([{ name: "article1" }]);
37 done(); 37 done();
38 }); 38 });
@@ -42,7 +42,7 @@ describe(&quot;Services&quot;, () =&gt; { @@ -42,7 +42,7 @@ describe(&quot;Services&quot;, () =&gt; {
42 it("should get articles by profile", (done) => { 42 it("should get articles by profile", (done) => {
43 let profileId = 1; 43 let profileId = 1;
44 $httpBackend.expectGET(`/api/v1/profiles/${profileId}/articles`).respond(200, { articles: [{ name: "article1" }] }); 44 $httpBackend.expectGET(`/api/v1/profiles/${profileId}/articles`).respond(200, { articles: [{ name: "article1" }] });
45 - articleService.getByProfile(<noosfero.Profile>{id: profileId}).then((result: noosfero.RestResult<noosfero.Article[]>) => { 45 + articleService.getByProfile(<noosfero.Profile>{ id: profileId }).then((result: noosfero.RestResult<noosfero.Article[]>) => {
46 expect(result.data).toEqual([{ name: "article1" }]); 46 expect(result.data).toEqual([{ name: "article1" }]);
47 done(); 47 done();
48 }); 48 });
@@ -52,7 +52,7 @@ describe(&quot;Services&quot;, () =&gt; { @@ -52,7 +52,7 @@ describe(&quot;Services&quot;, () =&gt; {
52 it("should get articles by profile with additional filters", (done) => { 52 it("should get articles by profile with additional filters", (done) => {
53 let profileId = 1; 53 let profileId = 1;
54 $httpBackend.expectGET(`/api/v1/profiles/${profileId}/articles?path=test`).respond(200, { articles: [{ name: "article1" }] }); 54 $httpBackend.expectGET(`/api/v1/profiles/${profileId}/articles?path=test`).respond(200, { articles: [{ name: "article1" }] });
55 - articleService.getByProfile(<noosfero.Profile>{id: profileId}, { path: 'test' }).then((result: noosfero.RestResult<noosfero.Article[]>) => { 55 + articleService.getByProfile(<noosfero.Profile>{ id: profileId }, { path: 'test' }).then((result: noosfero.RestResult<noosfero.Article[]>) => {
56 expect(result.data).toEqual([{ name: "article1" }]); 56 expect(result.data).toEqual([{ name: "article1" }]);
57 done(); 57 done();
58 }); 58 });
@@ -62,7 +62,7 @@ describe(&quot;Services&quot;, () =&gt; { @@ -62,7 +62,7 @@ describe(&quot;Services&quot;, () =&gt; {
62 it("should get article children with additional filters", (done) => { 62 it("should get article children with additional filters", (done) => {
63 let articleId = 1; 63 let articleId = 1;
64 $httpBackend.expectGET(`/api/v1/articles/${articleId}/children?path=test`).respond(200, { articles: [{ name: "article1" }] }); 64 $httpBackend.expectGET(`/api/v1/articles/${articleId}/children?path=test`).respond(200, { articles: [{ name: "article1" }] });
65 - articleService.getChildren(<noosfero.Article>{id: articleId}, { path: 'test' }).then((result: noosfero.RestResult<noosfero.Article[]>) => { 65 + articleService.getChildren(<noosfero.Article>{ id: articleId }, { path: 'test' }).then((result: noosfero.RestResult<noosfero.Article[]>) => {
66 expect(result.data).toEqual([{ name: "article1" }]); 66 expect(result.data).toEqual([{ name: "article1" }]);
67 done(); 67 done();
68 }); 68 });
@@ -71,14 +71,25 @@ describe(&quot;Services&quot;, () =&gt; { @@ -71,14 +71,25 @@ describe(&quot;Services&quot;, () =&gt; {
71 71
72 it("should create an article in a profile", (done) => { 72 it("should create an article in a profile", (done) => {
73 let profileId = 1; 73 let profileId = 1;
74 - let article: noosfero.Article = <any>{ id: null};  
75 - $httpBackend.expectPOST(`/api/v1/profiles/${profileId}/articles`, { article: article }).respond(200, {article: { id: 2 }});  
76 - articleService.createInProfile(<noosfero.Profile>{id: profileId}, article).then((result: noosfero.RestResult<noosfero.Article>) => { 74 + let article: noosfero.Article = <any>{ id: null };
  75 + $httpBackend.expectPOST(`/api/v1/profiles/${profileId}/articles`, { article: article }).respond(200, { article: { id: 2 } });
  76 + articleService.createInProfile(<noosfero.Profile>{ id: profileId }, article).then((result: noosfero.RestResult<noosfero.Article>) => {
77 expect(result.data).toEqual({ id: 2 }); 77 expect(result.data).toEqual({ id: 2 });
78 done(); 78 done();
79 }); 79 });
80 $httpBackend.flush(); 80 $httpBackend.flush();
81 }); 81 });
  82 +
  83 + it("should search for articles in environment", (done) => {
  84 + let profileId = 1;
  85 + let article: noosfero.Article = <any>{ id: null };
  86 + $httpBackend.expectGET(`/api/v1/search/article?query=query`).respond(200, { articles: [{ id: 2 }] });
  87 + articleService.search({ query: 'query' }).then((result: noosfero.RestResult<noosfero.Article[]>) => {
  88 + expect(result.data).toEqual([{ id: 2 }]);
  89 + done();
  90 + });
  91 + $httpBackend.flush();
  92 + });
82 }); 93 });
83 94
84 95
src/lib/ng-noosfero-api/http/article.service.ts
@@ -118,6 +118,11 @@ export class ArticleService extends RestangularService&lt;noosfero.Article&gt; { @@ -118,6 +118,11 @@ export class ArticleService extends RestangularService&lt;noosfero.Article&gt; {
118 return this.listSubElements(<noosfero.Article>articleElement, "children", params); 118 return this.listSubElements(<noosfero.Article>articleElement, "children", params);
119 } 119 }
120 120
  121 + search(params: any): ng.IPromise<noosfero.RestResult<noosfero.Article[]>> {
  122 + let deferred = this.$q.defer<noosfero.RestResult<noosfero.Article[]>>();
  123 + let restRequest = this.restangularService.all("search").customGET('article', params);
  124 + restRequest.then(this.getHandleSuccessFunction(deferred)).catch(this.getHandleErrorFunction(deferred));
  125 + return deferred.promise;
  126 + }
121 127
122 } 128 }
123 -