Compare View

switch
from
...
to
 
Commits (14)
README.md
... ... @@ -50,3 +50,31 @@ See some important folders bellow:
50 50 1. Create an app folder inside custom-theme and add your custom scss files
51 51 1. Put the templates that you want to override in the same structure from the main application source, e.g.:
52 52 `src/app/profile/profile.html` will be overriden by `themes/custom-theme/app/profile/profile.html`
  53 +
  54 +## Change skin
  55 +
  56 +- Create a any scss file into your theme folder structure
  57 +- Extend your skin css class from `%skin-base` scss placeholder selector. Something like this:
  58 +
  59 +```sass
  60 +.skin-custom {
  61 + @extend %skin-base
  62 +}
  63 +```
  64 +- Configure application to use the new theme, e.g.:
  65 +`npm config set angular-theme:skin custom-skin`
  66 +
  67 +- Start the application with `npm start` scripts ou make a build
  68 +
  69 +## Development environment
  70 +
  71 +## Known Issues
  72 +
  73 +### Message Translation: angular-i18n
  74 +
  75 + - Plural Interpolation only working when current language is En (English)
  76 +
  77 + `Plural Function not found for locale`
  78 +
  79 + For some reason the messageformat installed on bower_component directory was an older version. Removing the bower_components directory
  80 +and runing `bower install` solved the problem
... ...
bower.json
... ... @@ -47,9 +47,7 @@
47 47 "ng-ckeditor": {
48 48 "main": [
49 49 "ng-ckeditor.js",
50   - "libs/ckeditor/lang",
51   - "libs/ckeditor/ckeditor.js",
52   - "libs/ckeditor/config.js"
  50 + "libs/ckeditor/ckeditor.js"
53 51 ]
54 52 },
55 53 "bootstrap-sass": {
... ...
gulp/build.js
... ... @@ -30,7 +30,8 @@ gulp.task('partials', function () {
30 30 quotes: true
31 31 }))
32 32 .pipe($.angularTemplatecache('templateCacheHtml-'+partialPath+'.js', {
33   - module: 'noosferoApp',
  33 + module: 'noosfero.templates.' + partialPath,
  34 + standalone: true,
34 35 root: partialPath
35 36 }))
36 37 .pipe(gulp.dest(conf.paths.tmp + '/partials/')));
... ... @@ -81,7 +82,6 @@ gulp.task('html', ['inject', 'partials'], function () {
81 82 .pipe($.revReplace({prefix: noosferoThemePrefix}))
82 83 .pipe(htmlFilter)
83 84 .pipe($.replace('/bower_components/ng-ckeditor/libs/ckeditor/', noosferoThemePrefix + 'ng-ckeditor/libs/ckeditor/'))
84   - .pipe($.replace('/bower_components/ng-ckeditor/ng-ckeditor.min.js', noosferoThemePrefix + 'ng-ckeditor/ng-ckeditor.min.js'))
85 85 .pipe($.minifyHtml({
86 86 empty: true,
87 87 spare: true,
... ... @@ -103,7 +103,7 @@ gulp.task('fonts', function () {
103 103 });
104 104  
105 105 gulp.task('ckeditor', function () {
106   - conf.wiredep.exclude.push(/ckeditor/); // exclude ckeditor from build to improve performance
  106 + conf.wiredep.exclude.push(/bower_components\/ng-ckeditor\/libs\/ckeditor/); // exclude ckeditor from build to improve performance
107 107 return gulp.src(['bower_components/ng-ckeditor/**/*']).pipe(gulp.dest(path.join(conf.paths.dist, '/ng-ckeditor')));
108 108 });
109 109  
... ...
gulp/conf.js
... ... @@ -27,6 +27,10 @@ exports.configTheme = function(theme) {
27 27 exports.paths.theme = theme || "angular-default";
28 28 exports.paths.allSources = [exports.paths.src, path.join(exports.paths.themes, exports.paths.theme)];
29 29 exports.paths.dist = path.join("dist", exports.paths.theme);
  30 +
  31 + if(argv.skin) {
  32 + exports.paths.skin = argv.skin;
  33 + }
30 34 }
31 35 exports.configTheme(argv.theme);
32 36  
... ...
gulp/inject.js
... ... @@ -3,6 +3,8 @@
3 3 var path = require('path');
4 4 var gulp = require('gulp');
5 5 var conf = require('./conf');
  6 +var map = require('map-stream');
  7 +var transform = require('vinyl-transform');
6 8  
7 9 var $ = require('gulp-load-plugins')();
8 10  
... ... @@ -40,3 +42,34 @@ gulp.task('inject', ['scripts', 'styles'], function () {
40 42 .pipe(wiredep(_.extend({}, conf.wiredep)))
41 43 .pipe(gulp.dest(path.join(conf.paths.tmp, '/serve')));
42 44 });
  45 +
  46 +/**
  47 +* Replace the default theme skin to a npm config
  48 +*
  49 +* Uses "vinyl-transform" + "map-stream" to open the
  50 +* js file and rewrite the source file into the same
  51 +* destination
  52 +*
  53 +* @see https://www.npmjs.com/package/vinyl-transform
  54 +* @see https://www.npmjs.com/package/map-stream
  55 +*/
  56 +gulp.task('inject-skin', function () {
  57 +
  58 + if(conf.paths.skin) {
  59 +
  60 + $.util.log('Configured theme skin:', conf.paths.skin);
  61 +
  62 + var replaceSkin = transform(function(filename) {
  63 + return map(function(file, next) {
  64 + var contents = file.toString();
  65 + contents = contents.replace('skin-whbl', conf.paths.skin);
  66 + return next(null, contents);
  67 + });
  68 + });
  69 +
  70 + gulp.src(path.join(conf.paths.src,'./noosfero.js'))
  71 + .pipe(replaceSkin)
  72 + .pipe(gulp.dest(conf.paths.src));
  73 + }
  74 +
  75 +});
... ...
gulp/scripts.js
... ... @@ -9,7 +9,7 @@ var browserSync = require('browser-sync');
9 9 var $ = require('gulp-load-plugins')();
10 10  
11 11  
12   -gulp.task('scripts-reload', function() {
  12 +gulp.task('scripts-reload', ['inject-skin'], function() {
13 13 return buildScripts()
14 14 .pipe(browserSync.stream());
15 15 });
... ...
package.json
... ... @@ -8,10 +8,11 @@
8 8 "ng-forward": "0.0.1-alpha.12"
9 9 },
10 10 "config": {
11   - "theme": "angular-default"
  11 + "theme": "angular-default",
  12 + "skin": "skin-whbl"
12 13 },
13 14 "scripts": {
14   - "build": "webpack; gulp clean && gulp --theme=$npm_package_config_theme build",
  15 + "build": "webpack; gulp clean && gulp --theme=$npm_package_config_theme --skin=$npm_package_config_skin build",
15 16 "build-all": "gulp clean && for d in ./themes/* ; do (gulp --theme=`basename $d` build); done",
16 17 "webpack": "webpack",
17 18 "karma": "concurrently \"webpack -w\" \"karma start\"",
... ... @@ -23,7 +24,7 @@
23 24 "test": "webpack -w --test",
24 25 "jenkins": "webpack && karma start --single-run",
25 26 "postinstall": "bower install; typings install; cd dev-scripts; typings install; cd ../; npm run fix-jqlite",
26   - "start": "concurrently \"webpack -w\" \"gulp --theme=$npm_package_config_theme serve\"",
  27 + "start": "concurrently \"webpack -w\" \"gulp --theme=$npm_package_config_theme --skin=$npm_package_config_skin serve\"",
27 28 "generate-indexes": "ts-node --project ./dev-scripts ./dev-scripts/generate-index-modules.ts",
28 29 "fix-jqlite": "ts-node --project ./dev-scripts dev-scripts/fix-jqlite.ts"
29 30 },
... ... @@ -46,8 +47,8 @@
46 47 "gulp-eslint": "~1.0.0",
47 48 "gulp-filter": "~3.0.1",
48 49 "gulp-flatten": "~0.2.0",
49   - "gulp-insert": "^0.5.0",
50 50 "gulp-inject": "~3.0.0",
  51 + "gulp-insert": "^0.5.0",
51 52 "gulp-load-plugins": "~0.10.0",
52 53 "gulp-merge-json": "^0.4.0",
53 54 "gulp-minify-css": "~1.2.1",
... ... @@ -81,6 +82,7 @@
81 82 "karma-webpack": "^1.7.0",
82 83 "lodash": "~3.10.1",
83 84 "main-bower-files": "~2.9.0",
  85 + "map-stream": "0.0.6",
84 86 "merge-stream": "^1.0.0",
85 87 "on-build-webpack": "^0.1.0",
86 88 "phantomjs": "~1.9.18",
... ... @@ -96,6 +98,7 @@
96 98 "typescript": "^1.8.10",
97 99 "typings": "^0.6.8",
98 100 "uglify-save-license": "~0.4.1",
  101 + "vinyl-transform": "^1.0.0",
99 102 "webpack": "^1.12.14",
100 103 "wiredep": "~2.2.2",
101 104 "wrench": "~1.5.8",
... ...
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/index.ts
... ... @@ -6,7 +6,19 @@ import {AuthEvents} from "./login/auth-events";
6 6  
7 7 declare var moment: any;
8 8  
9   -angular.module('noosfero.init', []).config(noosferoModuleConfig).
  9 +// FIXME see a better way to declare template modules for dev mode
  10 +try {
  11 + angular.module('noosfero.templates.app');
  12 +} catch (error) {
  13 + angular.module('noosfero.templates.app', []);
  14 +}
  15 +try {
  16 + angular.module('noosfero.templates.plugins');
  17 +} catch (error) {
  18 + angular.module('noosfero.templates.plugins', []);
  19 +}
  20 +angular.module('noosfero.init', ['noosfero.templates.app', 'noosfero.templates.plugins']).
  21 + config(noosferoModuleConfig).
10 22 run(noosferoAngularRunBlock).
11 23 constant("moment", moment).
12 24 constant("AuthEvents", AuthEvents);
... ...
src/app/layout/navbar/navbar.html
... ... @@ -44,6 +44,9 @@
44 44 <language-selector class="nav navbar-nav navbar-right"></language-selector>
45 45 </ul>
46 46 <div ui-view="actions"></div>
  47 + <div class="nav navbar-nav search navbar-right">
  48 + <search-form></search-form>
  49 + </div>
47 50 </div>
48 51 </div>
49 52 </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/_layout.scss
... ... @@ -54,6 +54,5 @@ ul.nav &gt; li.toggler-container {
54 54 @include make-row();
55 55 margin-left: 0px;
56 56 margin-right: 0px;
57   - background-color: #eeeeee;
58   - background: linear-gradient(top, #eeeeee 0%, #c9d4e4 100%);
  57 + background-color: #edecec;
59 58 }
60 59 \ No newline at end of file
... ...
src/app/layout/scss/skins/_whbl.scss
... ... @@ -3,7 +3,7 @@ $whbl-sidebar-linkColor: #484848;
3 3 $whbl-primary-color: #1E96D0; //blue
4 4 $whbl-font-color: #16191c;
5 5  
6   -.skin-whbl {
  6 +%skin-base {
7 7 #header-navbar {
8 8 background-color: $whbl-primary-color;
9 9 }
... ... @@ -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 }
... ... @@ -321,7 +313,7 @@ $whbl-font-color: #16191c;
321 313 }
322 314  
323 315 @media (max-width: $break-sm-max) {
324   - .skin-whbl {
  316 + %skin-base {
325 317 #logo.navbar-brand > img.normal-logo.logo-white {
326 318 display: block;
327 319 }
... ... @@ -333,3 +325,7 @@ $whbl-font-color: #16191c;
333 325 }
334 326 }
335 327 }
  328 +
  329 +.skin-whbl {
  330 + @extend %skin-base;
  331 +}
... ...
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/app/shared/components/bootstrap-switcher/bootstrap-switcher.component.ts
... ... @@ -17,16 +17,16 @@ export interface BootstrapSwitcherItem {
17 17 </button>
18 18 </div>
19 19 `,
20   - inputs: ['label', 'options', 'defaultOption'],
  20 + inputs: ['activeClass', 'defaultClass', 'label', 'options', 'defaultOption'],
21 21 outputs: ['onSwitch']
22 22 })
23 23 export class BootstrapSwitcherComponent {
24   - @Input('activeClass') activeClass: string = 'active btn-danger';
25   - @Input('defaultClass') defaultClass: string = 'btn-default';
26   - @Input('label') label: string;
27   - @Input('options') options: BootstrapSwitcherItem[];
28   - @Input('defaultOption') defaultOption: BootstrapSwitcherItem;
29   - @Output('onSwitch') onSwitch: EventEmitter<BootstrapSwitcherItem> = new EventEmitter<BootstrapSwitcherItem>();
  24 + @Input() activeClass: string = 'active btn-danger';
  25 + @Input() defaultClass: string = 'btn-default';
  26 + @Input() label: string;
  27 + @Input() options: BootstrapSwitcherItem[];
  28 + @Input() defaultOption: BootstrapSwitcherItem;
  29 + @Output() onSwitch: EventEmitter<BootstrapSwitcherItem> = new EventEmitter<BootstrapSwitcherItem>();
30 30  
31 31 selectedOption: BootstrapSwitcherItem = null;
32 32  
... ...
src/index.html
... ... @@ -30,6 +30,7 @@
30 30 <script>
31 31 CKEDITOR_BASEPATH='/bower_components/ng-ckeditor/libs/ckeditor/';
32 32 </script>
  33 + <script src="/bower_components/ng-ckeditor/libs/ckeditor/ckeditor.js"></script>
33 34  
34 35 <!-- build:js(src) scripts/vendor.js -->
35 36 <!-- bower:js -->
... ... @@ -38,15 +39,12 @@
38 39 <!-- endbuild -->
39 40  
40 41 <!-- build:js({.tmp/serve,.tmp/partials,src}) scripts/app.js -->
41   - <script src="commons.js"></script>
42   - <script src="vendor.bundle.js"></script>
43   - <script src="noosfero.js"></script>
44 42 <!-- inject:partials -->
45 43 <!-- angular templates will be automatically converted in js and inserted here -->
46 44 <!-- endinject -->
  45 + <script src="commons.js"></script>
  46 + <script src="vendor.bundle.js"></script>
  47 + <script src="noosfero.js"></script>
47 48 <!-- endbuild -->
48   -
49   - <script src="/bower_components/ng-ckeditor/libs/ckeditor/ckeditor.js"></script>
50   - <script src="/bower_components/ng-ckeditor/ng-ckeditor.min.js"></script>
51 49 </body>
52 50 </html>
... ...
src/languages/en.json
... ... @@ -77,5 +77,8 @@
77 77 "profile.custom_footer.label": "Footer",
78 78 "designMode.label": "In Design",
79 79 "designMode.toggle.ON": "ON",
80   - "designMode.toggle.OFF": "OFF"
  80 + "designMode.toggle.OFF": "OFF",
  81 + "search.results.summary": "{results, plural, one{result} other{# results}}",
  82 + "search.results.query.label": "Search for:",
  83 + "search.results.query.placeholder": "Search"
81 84 }
... ...
src/languages/pt.json
... ... @@ -77,5 +77,8 @@
77 77 "profile.custom_footer.label": "Rodapé",
78 78 "designMode.label": "Modo de Edição",
79 79 "designMode.toggle.ON": "Ligado",
80   - "designMode.toggle.OFF": "Desligado"
  80 + "designMode.toggle.OFF": "Desligado",
  81 + "search.results.summary": "{results, plural, one{# resultado} other{# resultados}}",
  82 + "search.results.query.label": "Buscar:",
  83 + "search.results.query.placeholder": "Informe aqui sua busca"
81 84 }
... ...
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   -
... ...
src/plugins/comment_paragraph/hotspot/export-comment-button.component.spec.ts 0 → 100644
... ... @@ -0,0 +1,58 @@
  1 +import {ExportCommentButtonHotspotComponent} from "./export-comment-button.component";
  2 +import {ComponentTestHelper, createClass} from '../../../spec/component-test-helper';
  3 +import * as helpers from "../../../spec/helpers";
  4 +import {Provider} from 'ng-forward';
  5 +import {ComponentFixture} from 'ng-forward/cjs/testing/test-component-builder';
  6 +import {PermissionDirective} from '../../../app/shared/components/permission/permission.directive';
  7 +
  8 +let htmlTemplate = '<export-comment-button-hotspot [article]="ctrl.article"></export-comment-button-hotspot>';
  9 +
  10 +describe("Components", () => {
  11 + describe("Export Comment Button Hotspot Component", () => {
  12 +
  13 + let serviceMock = jasmine.createSpyObj("CommentParagraphService", ["getArticle"]);
  14 +
  15 + let providers = [new Provider('CommentParagraphService', { useValue: serviceMock })]
  16 + .concat(helpers.provideFilters('translateFilter'));
  17 + let helper: ComponentTestHelper<ExportCommentButtonHotspotComponent>;
  18 +
  19 + beforeEach(angular.mock.module("templates"));
  20 +
  21 + beforeEach((done) => {
  22 + let cls = createClass({
  23 + template: htmlTemplate,
  24 + directives: [ExportCommentButtonHotspotComponent, PermissionDirective],
  25 + providers: providers,
  26 + properties: {
  27 + article: {}
  28 + }
  29 + });
  30 + helper = new ComponentTestHelper<ExportCommentButtonHotspotComponent>(cls, done);
  31 + });
  32 +
  33 + it('return true when comment paragraph is active', () => {
  34 + helper.component.article = <noosfero.Article>{ setting: { comment_paragraph_plugin_activate: true } };
  35 + helper.detectChanges();
  36 + expect(helper.component.isActivated()).toBeTruthy();
  37 + expect(helper.all('.export-comment-button').length).toEqual(1);
  38 + });
  39 +
  40 + it('return false when comment paragraph is not active', () => {
  41 + expect(helper.component.isActivated()).toBeFalsy();
  42 + expect(helper.all('.export-comment-button').length).toEqual(0);
  43 + });
  44 +
  45 + it('return false when article has no setting attribute', () => {
  46 + helper.component.article = <noosfero.Article>{};
  47 + helper.detectChanges();
  48 + expect(helper.component.isActivated()).toBeFalsy();
  49 + expect(helper.all('.export-comment-button').length).toEqual(0);
  50 + });
  51 +
  52 + it('not display export comment button when user does not have permission', () => {
  53 + helper.component.article = <noosfero.Article>{ setting: { comment_paragraph_plugin_activate: true } };
  54 + helper.detectChanges();
  55 + expect(helper.find('.export-comment-button').attr('style')).toEqual("display: none; ");
  56 + });
  57 + });
  58 +});
... ...
src/plugins/comment_paragraph/hotspot/export-comment-button.component.ts 0 → 100644
... ... @@ -0,0 +1,23 @@
  1 +import { Input, Inject, Component } from "ng-forward";
  2 +import {Hotspot} from "../../../app/hotspot/hotspot.decorator";
  3 +import {CommentParagraphService} from "../http/comment-paragraph.service";
  4 +
  5 +@Component({
  6 + selector: "export-comment-button-hotspot",
  7 + templateUrl: "plugins/comment_paragraph/hotspot/export-comment-button.html",
  8 +})
  9 +@Inject(CommentParagraphService)
  10 +@Hotspot("article_extra_toolbar_buttons")
  11 +export class ExportCommentButtonHotspotComponent {
  12 +
  13 + @Input() article: noosfero.Article;
  14 + exportCommentPath: any;
  15 +
  16 + constructor(private commentParagraphService: CommentParagraphService) { }
  17 +
  18 + isActivated() {
  19 + this.exportCommentPath = ["/api/v1/articles/", this.article.id, "/comment_paragraph_plugin/export"].join("");
  20 + return this.article && this.article.setting && this.article.setting.comment_paragraph_plugin_activate;
  21 + }
  22 +
  23 +}
... ...
src/plugins/comment_paragraph/hotspot/export-comment-button.html 0 → 100644
... ... @@ -0,0 +1,5 @@
  1 +<a href='{{ctrl.exportCommentPath}}' target="_self"
  2 + class="btn btn-default btn-xs export-comment-button" ng-if="ctrl.isActivated()"
  3 + permission="ctrl.article.permissions" permission-action="allow_edit">
  4 + <i class="fa fa-fw fa-download"></i> {{"comment-paragraph-plugin.export" | translate}}
  5 +</a>
... ...
src/plugins/comment_paragraph/index.ts
1 1 import {AllowCommentComponent} from "./allow-comment/allow-comment.component";
  2 +import {ExportCommentButtonHotspotComponent} from "./hotspot/export-comment-button.component";
2 3 import {CommentParagraphFormHotspotComponent} from "./hotspot/comment-paragraph-form.component";
3 4 import {DiscussionEditorComponent} from "./article/cms/discussion-editor/discussion-editor.component";
4 5 import {CommentParagraphArticleContentHotspotComponent} from "./hotspot/article-content/article-content.component";
5 6 import {DiscussionBlockComponent} from "./block/discussion/discussion-block.component";
6 7  
7 8 export let mainComponents: any = [AllowCommentComponent, DiscussionEditorComponent, DiscussionBlockComponent];
8   -export let hotspots: any = [CommentParagraphFormHotspotComponent, CommentParagraphArticleContentHotspotComponent];
  9 +export let hotspots: any = [ExportCommentButtonHotspotComponent, CommentParagraphFormHotspotComponent, CommentParagraphArticleContentHotspotComponent];
... ...
src/plugins/comment_paragraph/languages/en.json
1 1 {
2 2 "comment-paragraph-plugin.title": "Paragraph Comments",
  3 + "comment-paragraph-plugin.export": "Export Comments",
3 4 "comment-paragraph-plugin.discussion.editor.start_date.label": "From",
4 5 "comment-paragraph-plugin.discussion.editor.end_date.label": "To",
5 6 "comment-paragraph-plugin.discussion.header": "Open for comments",
... ...
src/plugins/comment_paragraph/languages/pt.json
1 1 {
2 2 "comment-paragraph-plugin.title": "Comentários por Parágrafo",
  3 + "comment-paragraph-plugin.export": "Exportar Comentários",
3 4 "comment-paragraph-plugin.discussion.editor.start_date.label": "De",
4 5 "comment-paragraph-plugin.discussion.editor.end_date.label": "Até",
5 6 "comment-paragraph-plugin.discussion.header": "Aberto para comentários",
... ...
themes/angular-participa-consulta/app/blocks.scss
  1 +%panel-head-theme {
  2 + border: none;
  3 + border-radius: 3px 3px 0 0;
  4 + font-family: "Ubuntu Medium";
  5 + font-size: 15px;
  6 + font-variant: normal;
  7 + font-weight: normal;
  8 + line-height: 30px;
  9 + padding: 15px 15px 15px 15px;
  10 + text-transform: capitalize;
  11 + background-size: 30px 30px;
  12 + background: #DAE1C4 !important;
  13 + color: #4F9CAC;
  14 +}
  15 +
1 16 .block-head-with-icon {
2 17 padding: 15px 15px 15px 55px;
3 18 }
4 19  
  20 +.content-wrapper .block .panel-heading,
  21 +.content-wrapper .side-options .panel-heading {
  22 + @extend %panel-head-theme;
  23 +}
  24 +
5 25 .content-wrapper .block {
6   - .panel-heading {
7   - border: none;
8   - border-radius: 3px 3px 0 0;
9   - font-family: "Ubuntu Medium";
10   - font-size: 15px;
11   - font-variant: normal;
12   - font-weight: normal;
13   - line-height: 30px;
14   - padding: 15px 15px 15px 15px;
15   - text-transform: capitalize;
16   - background-size: 30px 30px;
17   - background: #DAE1C4;
18   - color: #4F9CAC;
19   - }
20 26  
21 27 &.membersblock {
22 28 .panel-heading {
... ...
themes/angular-participa-consulta/app/navbar.scss
  1 +.skin-yellow {
  2 + .navbar-inverse .navbar-toggle {
  3 + border-color: #D49F18;
  4 + }
  5 +
  6 + .container-fluid .navbar-header .navbar-toggle {
  7 + &:hover, &:focus {
  8 + background-color: #f5b025;
  9 + }
  10 + }
  11 +}
  12 +
1 13 .navbar {
2 14 min-height: 123px;
3 15 background-color: #f9c404;
... ...
themes/angular-participa-consulta/app/participa-consulta.scss
  1 +$selected-color: #f6c445;
  2 +
1 3 @font-face {
2 4 font-family: 'Ubuntu';
3 5 font-weight: 300;
... ... @@ -19,9 +21,51 @@
19 21 src: url('../assets/fonts/participa-consulta/Ubuntu-RI.ttf');
20 22 }
21 23  
22   -.skin-whbl .notifications-list .item-footer {
23   - background: #DAE1C4;
24   - color: #4F9CAC;
  24 +.skin-yellow {
  25 + @extend %skin-base;
  26 +
  27 + .notifications-list .item-footer {
  28 + background: #DAE1C4;
  29 + color: #4F9CAC;
  30 + }
  31 +
  32 + .navbar-nav .open > a,
  33 + .navbar-nav .open > a:hover,
  34 + .navbar-nav .open > a:focus {
  35 + background-color: $selected-color;
  36 + color: #000;
  37 + }
  38 +
  39 + .nav .open > a,
  40 + .nav .open > a:hover,
  41 + .nav .open > a:focus {
  42 + border: none;
  43 + }
  44 +
  45 + .dropdown-menu {
  46 + li > a:hover {
  47 + color: #555;
  48 + background-color: #F7F7F7;
  49 + }
  50 +
  51 + .active > a,
  52 + .active > a:hover,
  53 + .active > a:focus {
  54 + color: #000;
  55 + background-color: #CCC;
  56 + }
  57 + }
  58 +
  59 + .nav .caret {
  60 + border-bottom-color: #fff !important;
  61 + border-top-color: #fff !important;
  62 + }
  63 +
  64 + .nav .open .caret {
  65 + border-bottom-color: #000 !important;
  66 + border-top-color: #000 !important;
  67 + }
  68 +
25 69 }
26 70  
27 71 .profile-header, .profile-footer{
... ...