Commit d0fa7823b63317f1649830850aeb477bcabf7b18

Authored by Ábner Oliveira
2 parents 04a99db3 575daccf

Merge branch 'search' into 'master'

Component to search for articles in the environment

Issue #49

See merge request !37
src/app/environment/environment.component.ts
... ... @@ -2,6 +2,7 @@ import {StateConfig, Component, Inject, provide} from 'ng-forward';
2 2 import {EnvironmentService} from "../../lib/ng-noosfero-api/http/environment.service";
3 3 import {NotificationService} from "../shared/services/notification.service";
4 4 import {EnvironmentHomeComponent} from "./environment-home.component";
  5 +import {SearchComponent} from "../search/search.component";
5 6  
6 7 /**
7 8 * @ngdoc controller
... ... @@ -29,6 +30,18 @@ import {EnvironmentHomeComponent} from "./environment-home.component";
29 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 47 @Inject(EnvironmentService, "$state", "currentEnvironment")
... ...
src/app/layout/navbar/navbar.html
... ... @@ -43,6 +43,9 @@
43 43 <language-selector class="nav navbar-nav navbar-right"></language-selector>
44 44 </ul>
45 45 <div ui-view="actions"></div>
  46 + <div class="nav navbar-nav search navbar-right">
  47 + <search-form></search-form>
  48 + </div>
46 49 </div>
47 50 </div>
48 51 </nav>
... ...
src/app/layout/navbar/navbar.scss
... ... @@ -34,6 +34,16 @@
34 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 47 @media (max-width: $break-sm-max) {
38 48 .navbar-toggle .fa-bars{
39 49 font-size: 14pt;
... ...
src/app/layout/scss/skins/_whbl.scss
... ... @@ -180,27 +180,19 @@ $whbl-font-color: #16191c;
180 180 }
181 181 .pagination {
182 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 40 import {MainBlockComponent} from "../layout/blocks/main/main-block.component";
41 41 import {HtmlEditorComponent} from "../shared/components/html-editor/html-editor.component";
42 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 47 * @ngdoc controller
... ... @@ -100,7 +102,7 @@ export class EnvironmentContent {
100 102 LinkListBlockComponent, CommunitiesBlockComponent, HtmlEditorComponent, ProfileComponent,
101 103 MainBlockComponent, RecentDocumentsBlockComponent, Navbar, SidebarComponent, ProfileImageBlockComponent,
102 104 MembersBlockComponent, NoosferoTemplate, DateFormat, RawHTMLBlockComponent, StatisticsBlockComponent,
103   - LoginBlockComponent, CustomContentComponent, PermissionDirective
  105 + LoginBlockComponent, CustomContentComponent, PermissionDirective, SearchFormComponent, SearchComponent
104 106 ].concat(plugins.mainComponents).concat(plugins.hotspots),
105 107 providers: [AuthService, SessionService, NotificationService, BodyStateClassesService,
106 108 "ngAnimate", "ngCookies", "ngStorage", "ngTouch",
... ...
src/app/search/search-form/search-form.component.spec.ts 0 → 100644
... ... @@ -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,26 @@
  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 +
  15 + ngOnInit() {
  16 + this.query = this.$state.params['query'];
  17 + }
  18 +
  19 + search() {
  20 + this.$state.go('main.environment.search', { query: this.query });
  21 + }
  22 +
  23 + isSearchPage() {
  24 + return "main.environment.search" === this.$state.current.name;
  25 + }
  26 +}
... ...
src/app/search/search-form/search-form.html 0 → 100644
... ... @@ -0,0 +1,8 @@
  1 +<form class="navbar-form search-form" role="search" ng-if="!ctrl.isSearchPage()">
  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 @@
  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,40 @@
  1 +import {Component, Inject} from "ng-forward";
  2 +import {ArticleService} from "./../../lib/ng-noosfero-api/http/article.service";
  3 +
  4 +import {SearchFormComponent} from "./search-form/search-form.component";
  5 +
  6 +@Component({
  7 + selector: 'search',
  8 + templateUrl: 'app/search/search.html',
  9 + directives: [SearchFormComponent]
  10 +})
  11 +@Inject(ArticleService, "$stateParams", "$state")
  12 +export class SearchComponent {
  13 +
  14 + articles: noosfero.Article[];
  15 + query: string;
  16 + totalResults = 0;
  17 + perPage = 10;
  18 + currentPage: number = 0;
  19 +
  20 + constructor(private articleService: ArticleService, private $stateParams: ng.ui.IStateParamsService, private $state: ng.ui.IStateService) {
  21 + this.query = this.$stateParams['query'];
  22 + this.loadPage();
  23 + }
  24 +
  25 + search() {
  26 + this.$state.go('main.environment.search', { query: this.query });
  27 + }
  28 +
  29 + loadPage() {
  30 + let filters = {
  31 + query: this.query,
  32 + per_page: this.perPage,
  33 + page: this.currentPage
  34 + };
  35 + this.articleService.search(filters).then((result: noosfero.RestResult<noosfero.Article[]>) => {
  36 + this.totalResults = <number>result.headers("total");
  37 + this.articles = result.data;
  38 + });
  39 + }
  40 +}
... ...
src/app/search/search.html 0 → 100644
... ... @@ -0,0 +1,28 @@
  1 +<form ng-submit="ctrl.search()">
  2 +<label for="query" ng-bind-html="'search.results.query.label' | translate"></label>
  3 +<input id="query" placeholder="{{'search.results.query.placeholder' | translate}}" type="search" class="search-box-title" ng-model="ctrl.query">
  4 +</form>
  5 +<div class="search-results">
  6 + <div class="summary">
  7 + {{"search.results.summary" | translate:{results: ctrl.totalResults}:"messageformat"}}
  8 + </div>
  9 + <div ng-repeat="article in ctrl.articles | orderBy: 'created_at':true" class="result">
  10 + <a class="title" ui-sref="main.profile.page({profile: article.profile.identifier, page: article.path})">
  11 + <h4 ng-bind="article.title"></h4>
  12 + </a>
  13 + <div class="info">
  14 + <a class="profile" ui-sref="main.profile.home({profile: article.profile.identifier})">
  15 + {{article.profile.name}}
  16 + </a>
  17 + <span class="bullet-separator">•</span>
  18 + <span class="time">
  19 + <span am-time-ago="article.created_at | dateFormat"></span>
  20 + </span>
  21 + </div>
  22 + <div class="post-lead" ng-bind-html="article.body | stripTags | truncate: 250: '...': true"></div>
  23 + </div>
  24 + <uib-pagination ng-model="ctrl.currentPage" total-items="ctrl.totalResults" class="pagination-sm center-block"
  25 + boundary-links="true" items-per-page="ctrl.perPage" ng-change="ctrl.loadPage()"
  26 + first-text="«" last-text="»" previous-text="‹" next-text="›">
  27 + </uib-pagination>
  28 +</div>
... ...
src/app/search/search.scss 0 → 100644
... ... @@ -0,0 +1,48 @@
  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 +}
  31 +
  32 +.search-box-title {
  33 + border: 0;
  34 + border-bottom: 1px solid rgba(0,0,0,.15);
  35 + border-radius: 0;
  36 + font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,"Open Sans","Helvetica Neue",sans-serif;
  37 + letter-spacing: 0;
  38 + font-weight: 300;
  39 + font-style: normal;
  40 + font-size: 50px;
  41 + height: 80px;
  42 + padding: 0;
  43 + width: 100%;
  44 +}
  45 +
  46 +.search-box-title:focus {
  47 + outline: none;
  48 +}
0 49 \ No newline at end of file
... ...
src/languages/en.json
... ... @@ -74,5 +74,8 @@
74 74 "profile.content.success.message": "Profile saved!",
75 75 "custom_content.title": "Edit content",
76 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}}",
  79 + "search.results.query.label": "Search for:",
  80 + "search.results.query.placeholder": "Search"
78 81 }
... ...
src/languages/pt.json
... ... @@ -58,7 +58,7 @@
58 58 "article.remove.success.title": "Bom trabalho!",
59 59 "article.remove.success.message": "Artigo removido!",
60 60 "article.remove.confirmation.title": "Tem certeza?",
61   - "article.remove.confirmation.message": "Não será possível recuperar este artigo!",
  61 + "article.remove.confirmation.message": "Não será possível recuperar este artigo!",
62 62 "article.basic_editor.visibility": "Visibilidade",
63 63 "article.basic_editor.visibility.public": "Público",
64 64 "article.basic_editor.visibility.private": "Privado",
... ... @@ -74,5 +74,8 @@
74 74 "profile.content.success.message": "Perfil salvo!",
75 75 "custom_content.title": "Editar conteúdo",
76 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{# resultado} other{# resultados}}",
  79 + "search.results.query.label": "Buscar:",
  80 + "search.results.query.placeholder": "Informe aqui sua busca"
78 81 }
... ...
src/lib/ng-noosfero-api/http/article.service.spec.ts
... ... @@ -23,7 +23,7 @@ describe(&quot;Services&quot;, () =&gt; {
23 23 it("should remove article", (done) => {
24 24 let articleId = 1;
25 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 27 $httpBackend.flush();
28 28 $httpBackend.verifyNoOutstandingExpectation();
29 29 done();
... ... @@ -32,7 +32,7 @@ describe(&quot;Services&quot;, () =&gt; {
32 32 it("should return article children", (done) => {
33 33 let articleId = 1;
34 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 36 expect(result.data).toEqual([{ name: "article1" }]);
37 37 done();
38 38 });
... ... @@ -42,7 +42,7 @@ describe(&quot;Services&quot;, () =&gt; {
42 42 it("should get articles by profile", (done) => {
43 43 let profileId = 1;
44 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 46 expect(result.data).toEqual([{ name: "article1" }]);
47 47 done();
48 48 });
... ... @@ -52,7 +52,7 @@ describe(&quot;Services&quot;, () =&gt; {
52 52 it("should get articles by profile with additional filters", (done) => {
53 53 let profileId = 1;
54 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 56 expect(result.data).toEqual([{ name: "article1" }]);
57 57 done();
58 58 });
... ... @@ -62,7 +62,7 @@ describe(&quot;Services&quot;, () =&gt; {
62 62 it("should get article children with additional filters", (done) => {
63 63 let articleId = 1;
64 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 66 expect(result.data).toEqual([{ name: "article1" }]);
67 67 done();
68 68 });
... ... @@ -71,14 +71,25 @@ describe(&quot;Services&quot;, () =&gt; {
71 71  
72 72 it("should create an article in a profile", (done) => {
73 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 77 expect(result.data).toEqual({ id: 2 });
78 78 done();
79 79 });
80 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 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   -
... ...