├── .nvmrc ├── client ├── assets │ ├── components │ │ ├── login │ │ │ ├── _login.scss │ │ │ ├── login.html │ │ │ └── login.js │ │ ├── paginate │ │ │ ├── _paginate.scss │ │ │ ├── paginate.js │ │ │ ├── paginate.html │ │ │ ├── paginateService.js │ │ │ └── paginateDirective.js │ │ ├── document │ │ │ ├── document_web │ │ │ │ ├── _document_web.scss │ │ │ │ ├── document_web.js │ │ │ │ └── document_web.html │ │ │ ├── document_EXAMPLE │ │ │ │ ├── _document_EXAMPLE.scss │ │ │ │ ├── document_EXAMPLE.html │ │ │ │ └── document_EXAMPLE.js │ │ │ ├── document_default │ │ │ │ ├── _document_default.scss │ │ │ │ ├── document_default.html │ │ │ │ └── document_default.js │ │ │ ├── document_file │ │ │ │ ├── _document_file.scss │ │ │ │ ├── document_file.js │ │ │ │ └── document_file.html │ │ │ ├── document_twitter │ │ │ │ ├── _document_twitter.scss │ │ │ │ ├── document_twitter.html │ │ │ │ └── document_twitter.js │ │ │ ├── document_jira │ │ │ │ ├── document_jira.html │ │ │ │ ├── _document_jira.scss │ │ │ │ ├── contentTypes │ │ │ │ │ ├── jiraIssue.js │ │ │ │ │ ├── jiraProject.js │ │ │ │ │ ├── jiraProject.html │ │ │ │ │ └── jiraIssue.html │ │ │ │ └── document_jira.js │ │ │ ├── document_slack │ │ │ │ ├── _document_slack.scss │ │ │ │ ├── document_slack.html │ │ │ │ └── document_slack.js │ │ │ └── _document.scss │ │ ├── sort │ │ │ ├── _sort.scss │ │ │ ├── sort.html │ │ │ └── sort.js │ │ ├── landingPage │ │ │ ├── _landingPage.scss │ │ │ ├── landingPage.html │ │ │ └── landingPage.js │ │ ├── documentList │ │ │ ├── _documentList.scss │ │ │ ├── documentList.html │ │ │ └── documentList.js │ │ ├── field │ │ │ ├── _field.scss │ │ │ ├── field.html │ │ │ └── field.js │ │ ├── facetList │ │ │ ├── facetList.scss │ │ │ ├── facetList.html │ │ │ └── facetList.js │ │ ├── facetField │ │ │ ├── facetField.scss │ │ │ ├── facetField.html │ │ │ └── facetField.js │ │ ├── searchBox │ │ │ ├── searchBox.js │ │ │ ├── searchBox.html │ │ │ ├── searchBoxDataService.js │ │ │ ├── _searchBox.scss │ │ │ └── searchBoxDirective.js │ │ ├── _components.scss │ │ ├── components.js │ │ └── facetRange │ │ │ ├── facetRange.html │ │ │ ├── facetRange.js │ │ │ └── facetRangeService.js │ ├── img │ │ ├── favicon.ico │ │ ├── iconic │ │ │ ├── menu.svg │ │ │ ├── arrow_right.svg │ │ │ ├── arrow_down.svg │ │ │ ├── logo.svg │ │ │ ├── arrow_up.svg │ │ │ ├── arrow_left.svg │ │ │ ├── checkbox_off.svg │ │ │ ├── home.svg │ │ │ ├── empty_state.svg │ │ │ ├── cog.svg │ │ │ ├── magnifying-glass.svg │ │ │ ├── checkbox_on.svg │ │ │ ├── no_results.svg │ │ │ └── expand.svg │ │ └── logo │ │ │ └── lucidworks-white.svg │ ├── webfonts │ │ ├── Lato-Bold.eot │ │ ├── Lato-Bold.ttf │ │ ├── Lato-Bold.woff │ │ ├── Lato-Bold.woff2 │ │ ├── Lato-Italic.eot │ │ ├── Lato-Italic.ttf │ │ ├── Lato-Italic.woff │ │ ├── Lato-Italic.woff2 │ │ ├── Lato-Regular.eot │ │ ├── Lato-Regular.ttf │ │ ├── Lato-Regular.woff │ │ ├── Lato-BoldItalic.eot │ │ ├── Lato-BoldItalic.ttf │ │ ├── Lato-BoldItalic.woff │ │ ├── Lato-Regular.woff2 │ │ └── Lato-BoldItalic.woff2 │ ├── js │ │ ├── utils │ │ │ ├── utils.js │ │ │ └── DocsHelper.js │ │ ├── controllers │ │ │ ├── controllers.js │ │ │ └── LoginController.js │ │ ├── services │ │ │ ├── services.js │ │ │ ├── ApiBase.js │ │ │ ├── ClientStatsService.js │ │ │ ├── LandingPageService.js │ │ │ ├── AuthService.js │ │ │ ├── QueryService.js │ │ │ ├── DocumentService.js │ │ │ ├── SignalService.js │ │ │ ├── QueryDataService.js │ │ │ ├── AuthInterceptor.js │ │ │ └── URLService.js │ │ └── app.js │ ├── .eslintrc.js │ └── scss │ │ ├── app.scss │ │ └── _fonts.scss ├── index.html └── templates │ ├── login.html │ ├── home_main-content-frame.html │ └── home.html ├── .bowerrc ├── win64 ├── nssm.exe ├── installer │ ├── create-installer.cmd │ ├── instcr.cmd │ ├── Lucidworks-Glyph.ico │ ├── Lucidworks View Windows Installer Creation Guide.md │ └── inno-setup-installer.iss ├── uninstall-service.cmd ├── view.cmd ├── install.cmd └── getting-started-on-windows.md ├── tests ├── test_config.js └── assets │ └── js │ └── services │ ├── URLServiceTest.js │ ├── ConfigServiceTest.js │ └── QueryDataServiceTest.js ├── docs ├── Packaging.md ├── README.md ├── HowToStyleSearchUI.md ├── HowToUseQueryBuilder.md └── Customizing_Documents.md ├── .editorconfig ├── .gitignore ├── gulp ├── clean.js ├── version.js ├── lint.js ├── sass.js ├── watch.js ├── config.js ├── serve.js └── build.js ├── NOTICE ├── .eslintrc.js ├── view.sh ├── bower.json ├── .sass-lint.yml ├── .github └── workflows │ └── codeql-analysis.yml ├── karma.conf.js ├── package.json ├── CHANGELOG.md ├── gulpfile.js └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 5.2.0 2 | -------------------------------------------------------------------------------- /client/assets/components/login/_login.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/assets/components/paginate/_paginate.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components" 3 | } -------------------------------------------------------------------------------- /client/assets/components/document/document_web/_document_web.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/assets/components/document/document_EXAMPLE/_document_EXAMPLE.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/assets/components/document/document_default/_document_default.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /win64/nssm.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucidworks/lucidworks-view/HEAD/win64/nssm.exe -------------------------------------------------------------------------------- /win64/installer/create-installer.cmd: -------------------------------------------------------------------------------- 1 | start %~dp0instcr.cmd 2 | set /p go="Any key to continue" -------------------------------------------------------------------------------- /win64/installer/instcr.cmd: -------------------------------------------------------------------------------- 1 | "C:\Program Files (x86)\Inno Setup 5\compil32" /cc "%~dp0inno-setup-installer.iss" -------------------------------------------------------------------------------- /client/assets/components/sort/_sort.scss: -------------------------------------------------------------------------------- 1 | sort svg.iconic-sm { 2 | height: 16px; 3 | width: 16px; 4 | } 5 | -------------------------------------------------------------------------------- /client/assets/components/landingPage/_landingPage.scss: -------------------------------------------------------------------------------- 1 | .landing-page-list { 2 | list-style-type: none; 3 | } 4 | -------------------------------------------------------------------------------- /client/assets/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucidworks/lucidworks-view/HEAD/client/assets/img/favicon.ico -------------------------------------------------------------------------------- /client/assets/components/documentList/_documentList.scss: -------------------------------------------------------------------------------- 1 | #documentList{ 2 | em{ background-color:$highlight-color;} 3 | } 4 | -------------------------------------------------------------------------------- /client/assets/webfonts/Lato-Bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucidworks/lucidworks-view/HEAD/client/assets/webfonts/Lato-Bold.eot -------------------------------------------------------------------------------- /client/assets/webfonts/Lato-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucidworks/lucidworks-view/HEAD/client/assets/webfonts/Lato-Bold.ttf -------------------------------------------------------------------------------- /client/assets/webfonts/Lato-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucidworks/lucidworks-view/HEAD/client/assets/webfonts/Lato-Bold.woff -------------------------------------------------------------------------------- /client/assets/webfonts/Lato-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucidworks/lucidworks-view/HEAD/client/assets/webfonts/Lato-Bold.woff2 -------------------------------------------------------------------------------- /client/assets/webfonts/Lato-Italic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucidworks/lucidworks-view/HEAD/client/assets/webfonts/Lato-Italic.eot -------------------------------------------------------------------------------- /client/assets/webfonts/Lato-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucidworks/lucidworks-view/HEAD/client/assets/webfonts/Lato-Italic.ttf -------------------------------------------------------------------------------- /win64/installer/Lucidworks-Glyph.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucidworks/lucidworks-view/HEAD/win64/installer/Lucidworks-Glyph.ico -------------------------------------------------------------------------------- /client/assets/webfonts/Lato-Italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucidworks/lucidworks-view/HEAD/client/assets/webfonts/Lato-Italic.woff -------------------------------------------------------------------------------- /client/assets/webfonts/Lato-Italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucidworks/lucidworks-view/HEAD/client/assets/webfonts/Lato-Italic.woff2 -------------------------------------------------------------------------------- /client/assets/webfonts/Lato-Regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucidworks/lucidworks-view/HEAD/client/assets/webfonts/Lato-Regular.eot -------------------------------------------------------------------------------- /client/assets/webfonts/Lato-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucidworks/lucidworks-view/HEAD/client/assets/webfonts/Lato-Regular.ttf -------------------------------------------------------------------------------- /client/assets/webfonts/Lato-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucidworks/lucidworks-view/HEAD/client/assets/webfonts/Lato-Regular.woff -------------------------------------------------------------------------------- /tests/test_config.js: -------------------------------------------------------------------------------- 1 | appConfig = {//eslint-disable-line 2 | host: 'http://localhost', 3 | port: '8764', 4 | use_proxy: false 5 | }; 6 | -------------------------------------------------------------------------------- /client/assets/webfonts/Lato-BoldItalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucidworks/lucidworks-view/HEAD/client/assets/webfonts/Lato-BoldItalic.eot -------------------------------------------------------------------------------- /client/assets/webfonts/Lato-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucidworks/lucidworks-view/HEAD/client/assets/webfonts/Lato-BoldItalic.ttf -------------------------------------------------------------------------------- /client/assets/webfonts/Lato-BoldItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucidworks/lucidworks-view/HEAD/client/assets/webfonts/Lato-BoldItalic.woff -------------------------------------------------------------------------------- /client/assets/webfonts/Lato-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucidworks/lucidworks-view/HEAD/client/assets/webfonts/Lato-Regular.woff2 -------------------------------------------------------------------------------- /client/assets/webfonts/Lato-BoldItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucidworks/lucidworks-view/HEAD/client/assets/webfonts/Lato-BoldItalic.woff2 -------------------------------------------------------------------------------- /client/assets/js/utils/utils.js: -------------------------------------------------------------------------------- 1 | angular.module('lucidworksView.utils', [ 2 | 'lucidworksView.utils.queryBuilder', 3 | 'lucidworksView.utils.docs' 4 | ]); 5 | -------------------------------------------------------------------------------- /win64/uninstall-service.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | set NEXT_APP=lucidworks-view 4 | nssm stop %NEXT_APP% 5 | nssm remove %NEXT_APP% confirm 6 | 7 | sc.exe delete lucidworks-view 8 | -------------------------------------------------------------------------------- /docs/Packaging.md: -------------------------------------------------------------------------------- 1 | ## Building the the packaging 2 | 3 | ``` 4 | npm install 5 | bower install 6 | gulp build 7 | gulp cook 8 | gulp package --buildTarget=MACHINE_VARIANT 9 | ``` 10 | -------------------------------------------------------------------------------- /client/assets/components/field/_field.scss: -------------------------------------------------------------------------------- 1 | .field { 2 | //Style your document here 3 | .head-field{ 4 | display: inline; 5 | } 6 | .hide { 7 | display: none; 8 | } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /client/assets/js/controllers/controllers.js: -------------------------------------------------------------------------------- 1 | // Module initialization 2 | angular.module('lucidworksView.controllers', [ 3 | 'lucidworksView.controllers.home', 4 | 'lucidworksView.controllers.login' 5 | ]); 6 | -------------------------------------------------------------------------------- /client/assets/components/facetList/facetList.scss: -------------------------------------------------------------------------------- 1 | .facet-list div.accordion-title { 2 | display: flex; 3 | justify-content: space-between; 4 | } 5 | 6 | .block-list li.disabled>label:hover{ 7 | cursor: not-allowed; 8 | } 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_style = space 7 | indent_size = 2 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /win64/view.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | set NODEJS_EXE=%~dp0lib\nodejs\node.exe 4 | echo Checking node js version: 5 | %NODEJS_EXE% -v 6 | 7 | set thecmd=%1 8 | if [%1]==[] set thecmd=start 9 | 10 | %NODEJS_EXE% %~dp0node_modules\gulp\bin\gulp.js --production 11 | -------------------------------------------------------------------------------- /client/assets/components/paginate/paginate.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | // Initialize module. 4 | angular.module('lucidworksView.components.paginate', ['lucidworksView.services.config', 5 | 'lucidworksView.services.url', 'angular-humanize' 6 | ]); 7 | })(); 8 | -------------------------------------------------------------------------------- /client/assets/components/document/document_file/_document_file.scss: -------------------------------------------------------------------------------- 1 | .document-file { 2 | .detail { 3 | margin: .5rem 0; 4 | } 5 | .size, .mimetype, .owner, .last-modified { 6 | padding: .25rem; 7 | } 8 | .doc-label { 9 | font-weight: bold; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | .sass-cache 4 | *.iml 5 | bower_components 6 | build 7 | packages 8 | node_modules 9 | npm-debug.log 10 | tmp 11 | 12 | FUSION_CONFIG.js 13 | FUSION_CONFIG.*.js 14 | !FUSION_CONFIG.sample.js 15 | .jira-prefix 16 | .project 17 | .vscode 18 | installer/Output -------------------------------------------------------------------------------- /client/assets/components/field/field.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{!fc.showMore ? "..." : ""}} 5 | Read {{!fc.showMore ? "more" : "less"}} 6 | 7 | 8 | -------------------------------------------------------------------------------- /client/assets/components/document/document_twitter/_document_twitter.scss: -------------------------------------------------------------------------------- 1 | .document-twitter{ 2 | margin: 1rem 0; 3 | .quote { 4 | margin-bottom: .5rem; 5 | } 6 | .link { 7 | margin-left: 1.25rem; 8 | } 9 | .author { 10 | display: inline-block; 11 | margin-right: .5rem; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /client/assets/components/facetField/facetField.scss: -------------------------------------------------------------------------------- 1 | .block-list li.facet-active > label, 2 | .block-list li.facet-active:hover > label { 3 | color: $anchor-font-color; 4 | font-weight: bold; 5 | } 6 | .block-list li.facet-more-link a, 7 | .block-list li.facet-more-link a:hover { 8 | color: $anchor-font-color; 9 | 10 | } 11 | -------------------------------------------------------------------------------- /client/assets/components/landingPage/landingPage.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | See also: 4 | 7 |
8 |
9 | -------------------------------------------------------------------------------- /client/assets/components/document/document_jira/document_jira.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 | -------------------------------------------------------------------------------- /client/assets/components/searchBox/searchBox.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | // Initialize module. 4 | angular.module('lucidworksView.components.searchbox', [ 5 | 'lucidworksView.services.apiBase', 6 | 'lucidworksView.services.config', 7 | 'lucidworksView.services.query', 8 | 'lucidworksView.services.queryData' 9 | ]); 10 | })(); 11 | -------------------------------------------------------------------------------- /client/assets/components/_components.scss: -------------------------------------------------------------------------------- 1 | @import "document/document"; 2 | @import "documentList/documentList"; 3 | @import "facetField/facetField"; 4 | @import "facetList/facetList"; 5 | @import "field/field"; 6 | @import "landingPage/landingPage"; 7 | @import "login/login"; 8 | @import "paginate/paginate"; 9 | @import "searchBox/searchBox"; 10 | @import "sort/sort"; 11 | -------------------------------------------------------------------------------- /client/assets/components/searchBox/searchBox.html: -------------------------------------------------------------------------------- 1 | 8 | 9 | -------------------------------------------------------------------------------- /gulp/clean.js: -------------------------------------------------------------------------------- 1 | /*eslint-env node*/ 2 | var gulp = require('gulp'); 3 | var rimraf = require('rimraf'); 4 | 5 | // Cleans the build directory 6 | gulp.task('clean', function(cb) { 7 | rimraf('./build', cb); 8 | }); 9 | 10 | gulp.task('clean:templates', function(cb){ 11 | rimraf('./build/assets/js/templates.js', cb); 12 | }); 13 | 14 | gulp.task('clean:package', function(cb){ 15 | rimraf('./tmp/lucidworks-view', cb); 16 | }); 17 | -------------------------------------------------------------------------------- /client/assets/components/document/document_jira/_document_jira.scss: -------------------------------------------------------------------------------- 1 | .document-jira { 2 | margin-bottom: .75rem; 3 | .detail-row{ 4 | padding: .5rem 0; 5 | } 6 | .type, .key, .last-modified, .assignee, .lead{ 7 | display:inline-block; 8 | padding-right: .25rem; 9 | } 10 | .type { 11 | font-weight: bold; 12 | } 13 | .doc-label{ 14 | font-style: italic; 15 | padding-right: .25rem; 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /client/assets/components/login/login.html: -------------------------------------------------------------------------------- 1 |
2 | 6 | 10 | 11 | 12 | Log In 13 |
14 | -------------------------------------------------------------------------------- /client/assets/js/controllers/LoginController.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular 5 | .module('lucidworksView.controllers.login', ['lucidworksView.services.config']) 6 | .controller('LoginController', LoginController); 7 | 8 | function LoginController(ConfigService) { 9 | 'ngInject'; 10 | var vm = this; 11 | vm.appName = ConfigService.config.search_app_title; 12 | vm.logoLocation = ConfigService.config.logo_location; 13 | } 14 | })(); 15 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # View Docs 2 | 3 | Also see [README.md](../README.md) for instructions on installing and running Lucidworks View. 4 | 5 | * [How To Style The UI](HowToStyleSearchUI.md) 6 | 7 | All about customizing View's look and feel. 8 | 9 | * [Customizing Documents](Customizing_Documents.md) 10 | 11 | How to customize the templates that control how View displays different document types in search results. 12 | 13 | * [How to Use QueryBuilder](HowToUseQueryBuilder.md) 14 | -------------------------------------------------------------------------------- /client/assets/components/document/document_slack/_document_slack.scss: -------------------------------------------------------------------------------- 1 | .document-slack { 2 | margin: 1rem 0; 3 | .text, .user, .channel, .timestamp{ 4 | display: inline-block; 5 | padding-right: .25rem; 6 | } 7 | .text { 8 | color: $dark-color; 9 | margin-bottom: .5rem; 10 | } 11 | .channel, .timestamp .field{ 12 | margin-right: .25rem; 13 | } 14 | .doc-field { 15 | font-weight: bold; 16 | display:inline-block; 17 | padding: 0 .25rem 0 0; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /gulp/version.js: -------------------------------------------------------------------------------- 1 | /*eslint-env node*/ 2 | var $ = require('gulp-load-plugins')(); 3 | var gulp = require('gulp'); 4 | var argv = require('yargs').argv; 5 | 6 | gulp.task('version', function(){ 7 | return gulp.src(['package.json', 'bower.json']) 8 | .pipe($.jsonEditor(function(json) { 9 | if(argv.version){ 10 | json.version = argv.version; 11 | } 12 | return json; // must return JSON object. 13 | })) 14 | .pipe(gulp.dest('.')); 15 | }); 16 | -------------------------------------------------------------------------------- /gulp/lint.js: -------------------------------------------------------------------------------- 1 | /*eslint-env node*/ 2 | var $ = require('gulp-load-plugins')(); 3 | var gulp = require('gulp'); 4 | 5 | gulp.task('jslint', ['jslint:app', 'jslint:config']); 6 | 7 | gulp.task('jslint:app', function() { 8 | return gulp.src(global.paths.appJS) 9 | .pipe($.eslint()) 10 | .pipe($.eslint.format()); 11 | }); 12 | 13 | gulp.task('jslint:config', function(){ 14 | return gulp.src(global.paths.configJS) 15 | .pipe($.eslint({extends:'eslint:recommended'})) 16 | .pipe($.eslint.format()); 17 | }); 18 | -------------------------------------------------------------------------------- /client/assets/components/document/_document.scss: -------------------------------------------------------------------------------- 1 | @import "document_default/document_default"; 2 | @import "document_file/document_file"; 3 | @import "document_jira/document_jira"; 4 | @import "document_slack/document_slack"; 5 | @import "document_twitter/document_twitter"; 6 | @import "document_web/document_web"; 7 | // @import "document_EXAMPLE/document_EXAMPLE"; 8 | .document { 9 | //Style your document here 10 | .head-field{ 11 | display: inline; 12 | } 13 | .hide { 14 | display: none; 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /client/assets/components/facetList/facetList.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 |
6 |
7 | -------------------------------------------------------------------------------- /client/assets/img/iconic/menu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /client/assets/js/services/services.js: -------------------------------------------------------------------------------- 1 | // Module initialization 2 | angular.module('lucidworksView.services', [ 3 | 'lucidworksView.services.apiBase', 4 | 'lucidworksView.services.auth', 5 | 'lucidworksView.services.authInterceptor', 6 | 'lucidworksView.services.config', 7 | 'lucidworksView.services.document', 8 | 'lucidworksView.services.landingPage', 9 | 'lucidworksView.services.query', 10 | 'lucidworksView.services.queryData', 11 | 'lucidworksView.services.signals', 12 | 'lucidworksView.services.url', 13 | 'lucidworksView.services.localParams', 14 | 'lucidworksView.services.clientStats' 15 | ]); 16 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | 2 | Copyright 2016 Lucidworks Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | -------------------------------------------------------------------------------- /client/assets/components/document/document_EXAMPLE/document_EXAMPLE.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | 5 |

6 | 7 | 8 |

9 | 10 |

11 |
12 |
13 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable */ 2 | module.exports = { 3 | "rules": { 4 | "indent": ["error", 2, {"SwitchCase": 1 }], 5 | "quotes": [ "error", "single"], 6 | "linebreak-style": [ 7 | "error", 8 | "unix" 9 | ], 10 | "semi": [ 11 | "error", 12 | "always" 13 | ], 14 | "no-extra-semi": "warn", // disallow unnecessary semicolons 15 | "no-inner-declarations": "error", // disallow function or variable declarations in nested blocks 16 | }, 17 | "extends": [ 18 | "eslint:recommended" 19 | ], 20 | "globals": { 21 | "_": false, 22 | "global": false 23 | } 24 | }; 25 | /*eslint-enable */ 26 | -------------------------------------------------------------------------------- /client/assets/components/sort/sort.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Sort 4 | 5 | 11 | 12 | -------------------------------------------------------------------------------- /client/assets/img/iconic/arrow_right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /client/assets/img/iconic/arrow_down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /client/assets/img/iconic/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /client/assets/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable */ 2 | module.exports = { 3 | "rules": { 4 | "indent": [ 5 | 2, 6 | 2 7 | ], 8 | "quotes": [ 9 | 2, 10 | "single" 11 | ], 12 | "linebreak-style": [ 13 | 2, 14 | "unix" 15 | ], 16 | "semi": [ 17 | 2, 18 | "always" 19 | ], 20 | "no-extra-semi": 1, // disallow unnecessary semicolons 21 | "no-inner-declarations": 2, // disallow function or variable declarations in nested blocks 22 | }, 23 | "env": { 24 | "browser": true 25 | }, 26 | "extends": [ 27 | "angular", 28 | "eslint:recommended" 29 | ], 30 | "globals": { 31 | "_": false 32 | } 33 | }; 34 | /*eslint-enable */ 35 | -------------------------------------------------------------------------------- /view.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | echo -e " o O o\n" " o \|/ o\n" " o o \ | / o o\n" " \ \ \|/ / /\n" " (+++\@/+++)\n" " '---------'\n" " Lucidworks View" 4 | 5 | FILE="$(pwd)/lib/nodejs/bin/npm" 6 | if ! [ -f "$FILE" ]; 7 | then 8 | echo "Use npm instead of view.sh when not using packaged version of Lucidworks View." >&2 9 | exit 10 | fi 11 | 12 | if [ -z "$1" ]; 13 | then 14 | echo "" >&2 15 | echo "Usage: view.sh " >&2 16 | echo "" >&2 17 | echo "where is one of:" >&2 18 | echo " start, build" >&2 19 | exit 20 | fi 21 | 22 | 23 | # Put nodejs in the path for the duration of the script execution. 24 | export PATH=$(pwd)/lib/nodejs/bin:$PATH 25 | 26 | ./lib/nodejs/bin/npm $1 $2 $3 27 | -------------------------------------------------------------------------------- /client/assets/js/services/ApiBase.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular 5 | .module('lucidworksView.services.apiBase', []) 6 | .provider('ApiBase', ApiBase); 7 | 8 | 9 | function ApiBase() { 10 | 'ngInject'; 11 | var self = this; 12 | self.endpoint = ''; 13 | self.$get = get; 14 | self.setEndpoint = setEndpoint; 15 | self.getEndpoint = getEndpoint; 16 | 17 | function setEndpoint(endpoint) { 18 | self.endpoint = endpoint; 19 | } 20 | 21 | function getEndpoint() { 22 | return self.endpoint; 23 | } 24 | 25 | function get() { 26 | return { 27 | getEndpoint: getEndpoint, 28 | setEndpoint: setEndpoint 29 | }; 30 | } 31 | } 32 | })(); 33 | -------------------------------------------------------------------------------- /client/assets/img/iconic/arrow_up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /client/assets/img/iconic/arrow_left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /gulp/sass.js: -------------------------------------------------------------------------------- 1 | /*eslint-env node*/ 2 | var $ = require('gulp-load-plugins')(); 3 | var gulp = require('gulp'); 4 | // var browserSync = require('browser-sync').create(); 5 | 6 | // Compiles Sass 7 | gulp.task('sass', function () { 8 | var cssnano = $.if(global.isProduction, $.cssnano()); 9 | 10 | return gulp.src('client/assets/scss/app.scss') 11 | .pipe($.sass({ 12 | includePaths: global.paths.sass, 13 | outputStyle: (global.isProduction ? 'compressed' : 'nested'), 14 | errLogToConsole: true 15 | })) 16 | .pipe($.autoprefixer({ 17 | browsers: ['last 2 versions', 'ie 10'] 18 | })) 19 | .pipe(cssnano) 20 | .pipe($.plumber()) 21 | .pipe(gulp.dest('./build/assets/css/')); 22 | // .pipe(browserSync.stream()) 23 | }); 24 | -------------------------------------------------------------------------------- /client/assets/components/components.js: -------------------------------------------------------------------------------- 1 | angular.module('lucidworksView.components', [ 2 | 'lucidworksView.components.document', 3 | 'lucidworksView.components.document_file', 4 | 'lucidworksView.components.document_jira', 5 | 'lucidworksView.components.document_slack', 6 | 'lucidworksView.components.document_twitter', 7 | 'lucidworksView.components.document_web', 8 | 'lucidworksView.components.documentList', 9 | 'lucidworksView.components.facetField', 10 | 'lucidworksView.components.facetList', 11 | 'lucidworksView.components.facetRange', 12 | 'lucidworksView.components.field', 13 | 'lucidworksView.components.landingpage', 14 | 'lucidworksView.components.login', 15 | 'lucidworksView.components.paginate', 16 | 'lucidworksView.components.searchbox', 17 | 'lucidworksView.components.sort' 18 | ]); 19 | -------------------------------------------------------------------------------- /client/assets/img/iconic/checkbox_off.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /client/assets/js/services/ClientStatsService.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular 5 | .module('lucidworksView.services.clientStats', []) 6 | .factory('ClientStatsService', ClientStatsService); 7 | 8 | function ClientStatsService($window) { 9 | 'ngInject'; 10 | 11 | return { 12 | getBrowserLanguage: getBrowserLanguage, 13 | getBrowserPlatform: getBrowserPlatform, 14 | getBrowserUserAgent: getBrowserUserAgent 15 | }; 16 | 17 | /** 18 | * Returns information related to the client 19 | * @return {string} The value of the specific client information 20 | */ 21 | function getBrowserLanguage(){ 22 | return $window.navigator.language; 23 | } 24 | 25 | function getBrowserPlatform(){ 26 | return $window.navigator.platform; 27 | } 28 | 29 | function getBrowserUserAgent(){ 30 | return $window.navigator.userAgent; 31 | } 32 | } 33 | })(); 34 | -------------------------------------------------------------------------------- /client/assets/components/paginate/paginate.html: -------------------------------------------------------------------------------- 1 |
2 | 9 | 16 | 30 |
31 | -------------------------------------------------------------------------------- /client/templates/login.html: -------------------------------------------------------------------------------- 1 | --- 2 | name: login 3 | url: /login?query 4 | controller: LoginController as vm 5 | --- 6 |
7 | 8 | 9 | 19 | 20 | 21 |
22 |
23 |

Please login

24 | 25 |
26 |
27 | 28 |
29 | -------------------------------------------------------------------------------- /client/assets/img/iconic/home.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lucidworks-view", 3 | "version": "1.4.0", 4 | "authors": [ 5 | "Lucidworks" 6 | ], 7 | "description": "Lucidworks view: A easy to use Fusion powered UI for Search", 8 | "main": "gulpfile.js", 9 | "keywords": [ 10 | "framework", 11 | "angular", 12 | "web", 13 | "apps" 14 | ], 15 | "license": "Apache-2.0", 16 | "homepage": "https://github.com/lucidworks/lucidworks-view", 17 | "ignore": [ 18 | "**/.*", 19 | "node_modules", 20 | "bower_components", 21 | "test", 22 | "tests" 23 | ], 24 | "dependencies": { 25 | "foundation-apps": "~1.2.0", 26 | "lodash": "https://github.com/lodash/lodash.git", 27 | "ng-orwell": "~1.1.2", 28 | "angular-sanitize": "~1.4.10", 29 | "angular-rison": "~0.0.13", 30 | "angularjs-humanize": "master", 31 | "angular-mass-autocomplete": "^0.5.0", 32 | "angular-ui-router": "~0.4.2" 33 | }, 34 | "devDependencies": { 35 | "angular-mocks": "~1.4.10" 36 | }, 37 | "resolutions": { 38 | "angular": "1.3.x - 1.4.x", 39 | "angular-ui-router": "^0.4.2" 40 | } 41 | } -------------------------------------------------------------------------------- /client/assets/components/landingPage/landingPage.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular 5 | .module('lucidworksView.components.landingpage', ['lucidworksView.services.landingPage']) 6 | .directive('landingPage', landingPage); 7 | 8 | function landingPage() { 9 | 'ngInject'; 10 | return { 11 | controller: Controller, 12 | templateUrl: 'assets/components/landingPage/landingPage.html', 13 | controllerAs: 'lp', 14 | bindToController: true, 15 | scope: true 16 | }; 17 | 18 | } 19 | 20 | function Controller($scope, Orwell, LandingPageService) { 21 | 'ngInject'; 22 | var lp = this; 23 | lp.landingPages = false; 24 | 25 | var resultsObservable = Orwell.getObservable('queryResults'); 26 | 27 | resultsObservable.addObserver(function (data) { 28 | var landing_pages = LandingPageService.getLandingPagesFromData(data); 29 | if (angular.isArray(landing_pages)) { 30 | lp.landingPages = _.uniq(landing_pages); 31 | } else { 32 | lp.landingPages = false; 33 | } 34 | }); 35 | } 36 | 37 | })(); 38 | -------------------------------------------------------------------------------- /client/assets/components/document/document_jira/contentTypes/jiraIssue.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular 5 | .module('lucidworksView.components.document_jira') 6 | .directive('jiraIssue', jiraIssue); 7 | 8 | function jiraIssue() { 9 | 'ngInject'; 10 | var directive = { 11 | restrict: 'EA', 12 | templateUrl: 'assets/components/document/document_jira/contentTypes/jiraIssue.html', 13 | scope: true, 14 | controller: Controller, 15 | controllerAs: 'vm', 16 | bindToController: { 17 | doc: '=', 18 | highlight: '=' 19 | } 20 | }; 21 | 22 | return directive; 23 | 24 | } 25 | 26 | function Controller(DocumentService) { 27 | 'ngInject'; 28 | var vm = this; 29 | vm.postSignal = postSignal; 30 | vm.getTemplateDisplayFieldName = getTemplateDisplayFieldName; 31 | 32 | function postSignal(options){ 33 | DocumentService.postSignal(vm.doc._signals, options); 34 | } 35 | 36 | function getTemplateDisplayFieldName(field){ 37 | return DocumentService.getTemplateDisplayFieldName(vm.doc, field); 38 | } 39 | } 40 | })(); 41 | -------------------------------------------------------------------------------- /client/assets/components/document/document_jira/contentTypes/jiraProject.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular 5 | .module('lucidworksView.components.document_jira') 6 | .directive('jiraProject', jiraProject); 7 | 8 | function jiraProject() { 9 | 'ngInject'; 10 | var directive = { 11 | restrict: 'EA', 12 | templateUrl: 'assets/components/document/document_jira/contentTypes/jiraProject.html', 13 | scope: true, 14 | controller: Controller, 15 | controllerAs: 'vm', 16 | bindToController: { 17 | doc: '=', 18 | highlight: '=' 19 | } 20 | }; 21 | 22 | return directive; 23 | 24 | } 25 | 26 | 27 | function Controller(DocumentService) { 28 | 'ngInject'; 29 | var vm = this; 30 | vm.postSignal = postSignal; 31 | vm.getTemplateDisplayFieldName = getTemplateDisplayFieldName; 32 | 33 | function postSignal(options){ 34 | DocumentService.postSignal(vm.doc._signals, options); 35 | } 36 | 37 | function getTemplateDisplayFieldName(field){ 38 | return DocumentService.getTemplateDisplayFieldName(vm.doc, field); 39 | } 40 | } 41 | })(); 42 | -------------------------------------------------------------------------------- /client/assets/components/facetField/facetField.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{vm.facetLabel ? vm.facetLabel : vm.facetName}} 4 | Clear all 5 |
6 |
7 | 19 |
20 |
21 | -------------------------------------------------------------------------------- /client/assets/scss/app.scss: -------------------------------------------------------------------------------- 1 | @import "fonts"; 2 | @import "settings"; 3 | @import "foundation"; 4 | @import "../components/components"; 5 | 6 | #sidebar { 7 | //padding: 0 !important; 8 | //background: #f9f9f9 !important; 9 | background: #f9f9f9; 10 | border-right: 1px solid #e7e7e7; 11 | // transition-timing-function: cubic-bezier(.76, -.245, .24, 1.245); 12 | } 13 | 14 | .search-box-outer{ 15 | flex: 1; 16 | } 17 | 18 | .search-label{ 19 | margin-bottom: 0; 20 | } 21 | 22 | .card-result-comment { 23 | margin-top: .75rem; 24 | .card-get-started, 25 | .card-no-results { 26 | text-align: center; 27 | } 28 | } 29 | 30 | .label.secondary{ 31 | @extend %label; 32 | @include label-style($secondary-color, isitlight($secondary-color)); 33 | } 34 | 35 | .sort-button svg.iconic-sm { 36 | height: 16px; 37 | width: 16px; 38 | } 39 | 40 | @include breakpoint(medium) { 41 | .title-bar .right .action-sheet { 42 | width:250px; 43 | left:auto; 44 | right:0; 45 | top:100%; 46 | bottom:auto; 47 | transform:translateX(0) translateY(0); 48 | -webkit-transform:translateX(0) translateY(0); 49 | transition:none; 50 | &:before, &:after {left:auto;right:25px;} 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /docs/HowToStyleSearchUI.md: -------------------------------------------------------------------------------- 1 | # How To Style the UI 2 | 3 | 4 | Since Lucidworks View is built using Foundation for Apps, it should work with the same browsers: 5 | 6 | >http://foundation.zurb.com/apps/docs/#!/compatibility 7 | 8 | However, we have not tested or designed our default interface for all of them. 9 | 10 | ## Basic Styling 11 | 12 | * Set the logo and title 13 | 14 | Edit [`FUSION_CONFIG.js`](../FUSION_CONFIG.sample.js) and modify `search_app_title` and `logo_location`. 15 | 16 | * Configure the stylesheet 17 | 18 | Edit [`_settings.scss`](../client/assets/scss/_settings.scss) to customize stylesheet settings such as colors and markup. You can also turn off the styling of any SaSS component in this file. 19 | 20 | This file also contains settings for View-specific customizations such as the look and feel of the search box type-ahead. 21 | 22 | ## Advanced Styling 23 | 24 | View uses Foundation for Apps, a SaSS framework and AngularJs library for creating UIs. It provides layout components and an easy way to utilize state routing. It is also mobile-compatible and enables touch gestures for tablets or phones. 25 | 26 | The Foundation for Apps docs are here: 27 | 28 | >http://foundation.zurb.com/apps/docs 29 | -------------------------------------------------------------------------------- /client/assets/img/iconic/empty_state.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | empty_state 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.sass-lint.yml: -------------------------------------------------------------------------------- 1 | # Linter Options 2 | options: 3 | # Don't merge default rules 4 | merge-default-rules: false 5 | # Set the formatter to 'html' 6 | formatter: html 7 | # Output file instead of logging results 8 | output-file: 'linters/sass-lint.html' 9 | # File Options 10 | files: 11 | include: 'client/assets/**/*.s+(a|c)ss' 12 | # Rule Configuration 13 | rules: 14 | brace-style: 15 | style: 1tbs 16 | class-name-format: 17 | allow-leading-underscore: false 18 | convention: hyphenatedlowercase 19 | extends-before-mixins: 2 20 | extends-before-declarations: 2 21 | leading-zero: 22 | include: false 23 | placeholder-in-extend: 2 24 | mixins-before-declarations: 25 | - 2 26 | - 27 | exclude: 28 | - breakpoint 29 | - mq 30 | 31 | no-warn: 1 32 | no-debug: 1 33 | no-ids: 0 34 | no-important: 2 35 | hex-notation: 36 | - 2 37 | - 38 | style: lowercase 39 | indentation: 40 | - 2 41 | - 42 | size: 2 43 | property-sort-order: 44 | - 1 45 | - 46 | order: 47 | - display 48 | - margin 49 | ignore-custom-properties: true 50 | variable-for-property: 51 | - 2 52 | - 53 | properties: 54 | - margin 55 | - content 56 | -------------------------------------------------------------------------------- /client/assets/img/iconic/cog.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /client/assets/components/facetRange/facetRange.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{vm.facetLabel ? vm.facetLabel : vm.facetName}} 4 | Clear all 5 |
6 |
7 | 19 |
20 |
21 | -------------------------------------------------------------------------------- /client/assets/img/iconic/magnifying-glass.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /client/assets/js/services/LandingPageService.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular 5 | .module('lucidworksView.services.landingPage', []) 6 | .factory('LandingPageService', LandingPageService); 7 | 8 | function LandingPageService(Orwell, $window, ConfigService) { 9 | 'ngInject'; 10 | 11 | activate(); 12 | 13 | var service = { 14 | getLandingPagesFromData: getLandingPagesFromData 15 | }; 16 | 17 | return service; 18 | 19 | 20 | ////////////// 21 | 22 | /** 23 | * This activate() is to redirect the window the first landing-page 24 | * in case redirect flag in appConfig 25 | * is `true`. 26 | */ 27 | function activate() { 28 | var resultsObservable = Orwell.getObservable('queryResults'); 29 | 30 | resultsObservable.addObserver(function (data) { 31 | var landing_pages = service.getLandingPagesFromData(data); 32 | if (angular.isArray(landing_pages) && ConfigService.getLandingPageRedirect()) { 33 | $window.location.assign(landing_pages[0]); 34 | } 35 | }); 36 | } 37 | 38 | /** 39 | * Extracts landing pages from Fusion response data. 40 | */ 41 | function getLandingPagesFromData(data) { 42 | return _.get(data, 'fusion.landing-pages'); 43 | } 44 | 45 | } 46 | })(); 47 | -------------------------------------------------------------------------------- /client/assets/img/iconic/checkbox_on.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /win64/install.cmd: -------------------------------------------------------------------------------- 1 | echo You can safely ignore any warnings on the console and close the console when this process completes. 2 | 3 | set NEXT_APP=lucidworks-view 4 | echo Installing service using View installed at %~dp0 5 | %~dp0nssm stop %NEXT_APP% 6 | %~dp0nssm remove %NEXT_APP% confirm 7 | %~dp0nssm install %NEXT_APP% %~dp0view.cmd start 8 | %~dp0nssm set %NEXT_APP% AppDirectory %~dp0 9 | %~dp0nssm set %NEXT_APP% DisplayName Lucidworks View 10 | %~dp0nssm set %NEXT_APP% Description Lucidworks View Windows Service 11 | %~dp0nssm set %NEXT_APP% AppStdout %~dp0service.log 12 | %~dp0nssm set %NEXT_APP% AppStderr %~dp0service.log 13 | %~dp0nssm set %NEXT_APP% AppStdoutCreationDisposition 4 14 | %~dp0nssm set %NEXT_APP% AppStderrCreationDisposition 4 15 | %~dp0nssm set %NEXT_APP% AppRotateFiles 1 16 | %~dp0nssm set %NEXT_APP% AppRotateOnline 0 17 | %~dp0nssm set %NEXT_APP% AppRotateSeconds 86400 18 | %~dp0nssm set %NEXT_APP% AppRotateBytes 1048576 19 | 20 | cd %~dp0 21 | 22 | %~dp0lib\nodejs\npm install && %~dp0lib\nodejs\npm install -g gulp bower && %~dp0lib\nodejs\node %~dp0node_modules\bower\bin\bower install && %~dp0lib\nodejs\npm rebuild node-sass && %~dp0lib\nodejs\npm install -g gulp bower 23 | 24 | echo Reminder: You can safely ignore any warnings on the console and close the console when this process completes. 25 | 26 | %~dp0nssm set %NEXT_APP% DisplayName Lucidworks View 27 | -------------------------------------------------------------------------------- /client/assets/components/login/login.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular 5 | .module('lucidworksView.components.login', ['lucidworksView.services.auth', 6 | 'ui.router' 7 | ]) 8 | .constant('QUERY_PARAM', 'query') 9 | .directive('login', login); 10 | 11 | function login() { 12 | 'ngInject'; 13 | return { 14 | controller: Controller, 15 | templateUrl: 'assets/components/login/login.html', 16 | controllerAs: 'vm', 17 | bindToController: true, 18 | scope: true 19 | }; 20 | 21 | } 22 | 23 | function Controller(ConfigService, Orwell, AuthService, $state, QUERY_PARAM) { 24 | 'ngInject'; 25 | var vm = this; 26 | vm.username = ''; 27 | vm.password = ''; 28 | vm.error = null; 29 | vm.submitting = false; 30 | 31 | vm.submit = submit; 32 | 33 | function submit() { 34 | vm.error = null; 35 | vm.submitting = true; 36 | AuthService 37 | .createSession(vm.username, vm.password) 38 | .then(success, failure); 39 | 40 | function success() { 41 | vm.submitting = false; 42 | var params = {}; 43 | if ($state.params[QUERY_PARAM]) { 44 | params[QUERY_PARAM] = $state.params[QUERY_PARAM]; 45 | } 46 | $state.go('home', params); 47 | } 48 | 49 | function failure(err) { 50 | vm.submitting = false; 51 | vm.error = err; 52 | } 53 | 54 | } 55 | } 56 | })(); 57 | -------------------------------------------------------------------------------- /client/assets/components/document/document_file/document_file.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular 5 | .module('lucidworksView.components.document_file', ['lucidworksView.services.signals', 'angular-humanize']) 6 | .directive('documentFile', documentFile); 7 | 8 | function documentFile() { 9 | 'ngInject'; 10 | var directive = { 11 | restrict: 'EA', 12 | templateUrl: 'assets/components/document/document_file/document_file.html', 13 | scope: true, 14 | controller: Controller, 15 | controllerAs: 'vm', 16 | bindToController: { 17 | doc: '=', 18 | position: '=', 19 | highlight: '=' 20 | } 21 | }; 22 | 23 | return directive; 24 | 25 | } 26 | 27 | function Controller(DocumentService) { 28 | 'ngInject'; 29 | var vm = this; 30 | var templateFields = ['id', 'length', 'mimeType', 'owner', 'lastModified']; 31 | vm.getTemplateDisplayFieldName = getTemplateDisplayFieldName; 32 | 33 | activate(); 34 | 35 | function activate() { 36 | vm.doc = processDocument(vm.doc); 37 | } 38 | 39 | function processDocument(doc) { 40 | //set properties needed for display 41 | doc._templateDisplayFields = DocumentService.setTemplateDisplayFields(doc, templateFields); 42 | 43 | return doc; 44 | } 45 | 46 | function getTemplateDisplayFieldName(field){ 47 | return DocumentService.getTemplateDisplayFieldName(vm.doc, field); 48 | } 49 | } 50 | })(); 51 | -------------------------------------------------------------------------------- /client/assets/components/document/document_twitter/document_twitter.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 14 |
15 |
16 | -------------------------------------------------------------------------------- /client/assets/components/document/document_file/document_file.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | size: 7 |
8 |
9 | mimetype: 11 |
12 |
13 | owner: 15 |
16 |
17 | last modified: UTC 18 |
19 | 20 |
21 |
22 |
23 | -------------------------------------------------------------------------------- /gulp/watch.js: -------------------------------------------------------------------------------- 1 | /*eslint-env node*/ 2 | var gulp = require('gulp'); 3 | var sequence = require('run-sequence'); 4 | 5 | gulp.task('watch', function(){ 6 | // Watch Sass 7 | gulp.watch(['./client/assets/scss/**/*', './scss/**/*'], ['sass-watch']); 8 | 9 | // Watch JavaScript 10 | gulp.watch(['./client/assets/js/**/*', './js/**/*'], ['javascript']); 11 | 12 | // Watch Components 13 | gulp.watch(['./client/assets/components/**/*'], ['components']); 14 | 15 | // Watch static files 16 | gulp.watch(global.paths.assets, ['staticfiles']); 17 | 18 | // Watch app templates 19 | gulp.watch(['./client/templates/**/*.html'], ['template:sequence']); 20 | 21 | // Watch config 22 | gulp.watch(global.paths.configJS, ['jslint:config','copy:config', 'reloadBrowsers']); 23 | 24 | // Watch config sample 25 | gulp.watch(global.paths.configJSSample, ['copy:configSample', 'reloadBrowsers']); 26 | }); 27 | 28 | gulp.task('components', function(cb){ 29 | sequence('uglify:app', ['copy', 'sass'], 'reloadBrowsers', cb); 30 | }); 31 | 32 | gulp.task('staticfiles', function(cb){ 33 | sequence('copy', 'reloadBrowsers', cb); 34 | }); 35 | 36 | gulp.task('template:sequence', function(cb){ 37 | sequence('clean:templates', ['copy:foundation', 'copy:templates' /*, 'template:routes'*/], /*'concat:components',*/ 'reloadBrowsers', cb); 38 | }); 39 | 40 | gulp.task('javascript', function(cb){ 41 | sequence('jslint:app','uglify:app', 'reloadBrowsers', cb); 42 | }); 43 | 44 | gulp.task('sass-watch', function(cb){ 45 | sequence('sass', 'reloadBrowsers', cb); 46 | }); 47 | -------------------------------------------------------------------------------- /client/assets/components/document/document_jira/document_jira.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular 5 | .module('lucidworksView.components.document_jira', ['lucidworksView.services.signals']) 6 | .directive('documentJira', documentJira); 7 | 8 | function documentJira() { 9 | 'ngInject'; 10 | var directive = { 11 | restrict: 'EA', 12 | templateUrl: 'assets/components/document/document_jira/document_jira.html', 13 | scope: true, 14 | controller: Controller, 15 | controllerAs: 'vm', 16 | bindToController: { 17 | doc: '=', 18 | position: '=', 19 | highlight: '=' 20 | } 21 | }; 22 | 23 | return directive; 24 | 25 | } 26 | 27 | 28 | function Controller(DocumentService) { 29 | 'ngInject'; 30 | var vm = this; 31 | 32 | //define list of fields necessary to display the doc in the template (jira issue + jira project) 33 | var templateFields = ['id', 'summary', 'content', 'name', 'parent', 'jira_content_type', 'key', 'lastModified', 'assignee', 'lead']; 34 | 35 | activate(); 36 | 37 | function activate() { 38 | vm.doc = processDocument(vm.doc); 39 | } 40 | 41 | function processDocument(doc) { 42 | //set properties needed for display 43 | doc._templateDisplayFields = DocumentService.setTemplateDisplayFields(doc,templateFields); 44 | 45 | //set properties needed for signals 46 | doc._signals = DocumentService.setSignalsProperties(doc,vm.position); 47 | 48 | return doc; 49 | } 50 | 51 | } 52 | })(); 53 | -------------------------------------------------------------------------------- /client/assets/components/facetField/facetField.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular 5 | .module('lucidworksView.components.facetField', [ 6 | 'lucidworksView.services.config', 7 | 'foundation.core', 8 | 'lucidworksView.utils.queryBuilder', 9 | 'angular-humanize' 10 | ]) 11 | .config(Config); 12 | 13 | function Config(QueryBuilderProvider){ 14 | 'ngInject'; 15 | // Register transformers because facet fields can have funky URL syntax. 16 | QueryBuilderProvider.registerTransformer('keyValue', 'fq:field', fqFieldkeyValueTransformer); 17 | QueryBuilderProvider.registerTransformer('preEncodeWrapper', 'fq:field', fqFieldPreEncodeWrapper); 18 | QueryBuilderProvider.registerTransformer('encode', 'fq:field', fqFieldEncode); 19 | QueryBuilderProvider.registerTransformer('wrapper', 'fq:field', fqFieldWrapper); 20 | 21 | 22 | /** 23 | * Transformers. 24 | * 25 | * These will transform the output of the query, when the query is created. 26 | */ 27 | 28 | function fqFieldkeyValueTransformer(key, value) { 29 | var escapedKey = QueryBuilderProvider.escapeSpecialChars(key); 30 | return QueryBuilderProvider.keyValueString(escapedKey, value, ':'); 31 | } 32 | 33 | function fqFieldPreEncodeWrapper(data){ 34 | return '"'+data+'"'; 35 | } 36 | 37 | function fqFieldEncode(data){ 38 | return QueryBuilderProvider.encodeURIComponentPlus(data); 39 | } 40 | 41 | function fqFieldWrapper(data){ 42 | return '('+data+')'; 43 | } 44 | } 45 | 46 | })(); 47 | -------------------------------------------------------------------------------- /client/assets/components/searchBox/searchBoxDataService.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular 5 | .module('lucidworksView.components.searchbox') 6 | .factory('SearchBoxDataService', SearchBoxDataService); 7 | 8 | function SearchBoxDataService($http, $q, ConfigService, ApiBase, QueryBuilder, QueryDataService){ 9 | 'ngInject'; 10 | 11 | return { 12 | getTypeaheadResults: getTypeaheadResults 13 | }; 14 | 15 | //////////// 16 | 17 | function getTypeaheadResults(query){ 18 | var deferred = $q.defer(); 19 | 20 | var queryString = QueryBuilder.objectToURLString(query); 21 | 22 | var fullUrl = getQueryUrl(ConfigService.getIfTypeaheadQueryProfile()) + '?' + queryString; 23 | 24 | $http 25 | .get(fullUrl) 26 | .then(success) 27 | .catch(failure); 28 | 29 | function success(response) { 30 | deferred.resolve(response.data); 31 | } 32 | 33 | function failure(err) { 34 | deferred.reject(err.data); 35 | } 36 | 37 | return deferred.promise; 38 | } 39 | 40 | /** 41 | * Private function 42 | */ 43 | function getQueryUrl(isProfile){ 44 | var requestHandler = ConfigService.getTypeaheadRequestHandler(); 45 | 46 | var profileUrl = QueryDataService.getProfileEndpoint(ConfigService.getTypeaheadProfile(), requestHandler); 47 | 48 | var pipelineUrl = QueryDataService.getPipelineEndpoint(ConfigService.getTypeaheadPipeline(), requestHandler); 49 | 50 | return isProfile?profileUrl:pipelineUrl; 51 | } 52 | 53 | } 54 | })(); 55 | -------------------------------------------------------------------------------- /client/assets/img/iconic/no_results.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | no_results 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /client/assets/img/iconic/expand.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /client/assets/components/document/document_EXAMPLE/document_EXAMPLE.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular 5 | .module('lucidworksView.components.document_EXAMPLE', ['lucidworksView.services.signals']) 6 | .directive('documentExample', documentExample); 7 | 8 | function documentExample() { 9 | 'ngInject'; 10 | var directive = { 11 | restrict: 'EA', 12 | templateUrl: 'assets/components/document/document_EXAMPLE/document_EXAMPLE.html', 13 | scope: true, 14 | controller: Controller, 15 | controllerAs: 'vm', 16 | bindToController: { 17 | doc: '=', 18 | highlight: '=' 19 | } 20 | }; 21 | 22 | return directive; 23 | 24 | } 25 | 26 | function Controller(DocumentService) { 27 | 'ngInject'; 28 | var vm = this; 29 | var templateFields = Object.keys(vm.doc); 30 | vm.getTemplateDisplayFieldName = getTemplateDisplayFieldName; 31 | 32 | activate(); 33 | 34 | function activate() { 35 | vm.doc = processDocument(vm.doc); 36 | } 37 | 38 | function processDocument(doc) { 39 | //set properties needed for display 40 | doc._templateDisplayFields = DocumentService.setTemplateDisplayFields(doc, templateFields); 41 | doc._templateDisplayFields._lw_id_decoded = DocumentService.decodeFieldValue(doc._templateDisplayFields, 'id'); 42 | 43 | //set properties needed for signals 44 | doc._signals = DocumentService.setSignalsProperties(doc, vm.position); 45 | 46 | return doc; 47 | } 48 | 49 | function getTemplateDisplayFieldName(field){ 50 | return DocumentService.getTemplateDisplayFieldName(vm.doc, field); 51 | } 52 | } 53 | })(); 54 | -------------------------------------------------------------------------------- /client/assets/components/document/document_slack/document_slack.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 | 7 |
8 |
9 |
10 | 12 |
13 |
14 | @ 15 |
16 |
17 | 18 | UTC 19 | 20 |
21 |
22 |
23 |
24 |
25 | -------------------------------------------------------------------------------- /client/assets/components/document/document_twitter/document_twitter.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular 5 | .module('lucidworksView.components.document_twitter', ['lucidworksView.services.signals']) 6 | .directive('documentTwitter', documentTwitter); 7 | 8 | function documentTwitter() { 9 | 'ngInject'; 10 | var directive = { 11 | restrict: 'EA', 12 | templateUrl: 'assets/components/document/document_twitter/document_twitter.html', 13 | scope: true, 14 | controller: Controller, 15 | controllerAs: 'vm', 16 | bindToController: { 17 | doc: '=', 18 | position: '=', 19 | highlight: '=' 20 | } 21 | }; 22 | 23 | return directive; 24 | 25 | } 26 | 27 | function Controller(DocumentService) { 28 | 'ngInject'; 29 | var vm = this; 30 | var templateFields = ['id', 'createdAt', 'tweet', 'userLang', 'userScreenName']; 31 | vm.postSignal = postSignal; 32 | vm.getTemplateDisplayFieldName = getTemplateDisplayFieldName; 33 | 34 | activate(); 35 | 36 | function activate() { 37 | vm.doc = processDocument(vm.doc); 38 | } 39 | 40 | function processDocument(doc) { 41 | //set properties needed for display 42 | doc._templateDisplayFields = DocumentService.setTemplateDisplayFields(doc, templateFields); 43 | 44 | //set properties needed for signals 45 | doc._signals = DocumentService.setSignalsProperties(doc, vm.position); 46 | 47 | return doc; 48 | } 49 | 50 | function postSignal(options){ 51 | DocumentService.postSignal(vm.doc._signals, options); 52 | } 53 | 54 | function getTemplateDisplayFieldName(field){ 55 | return DocumentService.getTemplateDisplayFieldName(vm.doc, field); 56 | } 57 | } 58 | })(); 59 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "Code scanning - action" 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: '0 16 * * 3' 8 | 9 | jobs: 10 | CodeQL-Build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v2 17 | with: 18 | # We must fetch at least the immediate parents so that if this is 19 | # a pull request then we can checkout the head. 20 | fetch-depth: 2 21 | 22 | # If this run was triggered by a pull request event, then checkout 23 | # the head of the pull request instead of the merge commit. 24 | - run: git checkout HEAD^2 25 | if: ${{ github.event_name == 'pull_request' }} 26 | 27 | # Initializes the CodeQL tools for scanning. 28 | - name: Initialize CodeQL 29 | uses: github/codeql-action/init@v1 30 | # Override language selection by uncommenting this and choosing your languages 31 | # with: 32 | # languages: go, javascript, csharp, python, cpp, java 33 | 34 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 35 | # If this step fails, then you should remove it and run the build manually (see below) 36 | - name: Autobuild 37 | uses: github/codeql-action/autobuild@v1 38 | 39 | # ℹ️ Command-line programs to run using the OS shell. 40 | # 📚 https://git.io/JvXDl 41 | 42 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 43 | # and modify them (or add more) to build your code if your project 44 | # uses a compiled language 45 | 46 | #- run: | 47 | # make bootstrap 48 | # make release 49 | 50 | - name: Perform CodeQL Analysis 51 | uses: github/codeql-action/analyze@v1 52 | -------------------------------------------------------------------------------- /tests/assets/js/services/URLServiceTest.js: -------------------------------------------------------------------------------- 1 | /*global ngDescribe, describe, it, expect, spyOn, beforeEach*/ 2 | ngDescribe({ 3 | name: 'URLService', 4 | modules: 'lucidworksView.services', 5 | inject: ['URLService', '$rison', '$state', '$location', 'QueryService'], 6 | mocks: { 7 | ng:{ 8 | $location: { 9 | search: function(){ 10 | return {query: '(somerawquery:blah,blah:(0:one,1:(notarray:fine,array:(0:click,1:noclick))))'}; 11 | } 12 | }, 13 | $state: { 14 | go: function(){} 15 | } 16 | } 17 | }, 18 | tests: function(deps){ 19 | beforeEach(function(){ 20 | spyOn(deps.$rison, 'stringify').and.callThrough(); 21 | spyOn(deps.$rison, 'parse').and.callThrough(); 22 | spyOn(deps.$state, 'go').and.callThrough(); 23 | spyOn(deps.$location, 'search').and.callThrough(); 24 | spyOn(deps.QueryService, 'setQuery'); 25 | }); 26 | 27 | it('getQueryFromUrl should make the proper calls with proper parsing', function(){ 28 | var stuff = deps.URLService.getQueryFromUrl(); 29 | expect(deps.$location.search).toHaveBeenCalled(); 30 | expect(deps.$rison.parse).toHaveBeenCalledWith('(somerawquery:blah,blah:(0:one,1:(notarray:fine,array:(0:click,1:noclick))))'); 31 | expect(stuff).toEqual({somerawquery:'blah',blah:['one', {notarray: 'fine', array: ['click','noclick']}]}); 32 | }); 33 | 34 | it('setQuery should make the right calls as well', function(){ 35 | deps.QueryService.setQuery({}); 36 | expect(deps.$state.go).toHaveBeenCalledWith('home', {query:'(q:\'*\',start:0,wt:json)'}, {notify: false, reloadOnSearch: false}); 37 | expect(deps.$rison.stringify).toHaveBeenCalledWith({ 38 | q:'*', 39 | start: 0, 40 | wt: 'json' 41 | }); 42 | }); 43 | } 44 | }); 45 | -------------------------------------------------------------------------------- /gulp/config.js: -------------------------------------------------------------------------------- 1 | /*eslint-env node*/ 2 | /*eslint no-console:0*/ 3 | var $ = require('gulp-load-plugins')(); 4 | var gulp = require('gulp'); 5 | var fs = require('fs'); 6 | 7 | // Copies your app's config 8 | gulp.task('copy:config', function() { 9 | if(fs.existsSync(global.paths.configJS[0])){ //If the file exists use that, or copy from the sample 10 | return gulp.src(global.paths.configJS).pipe(gulp.dest('./build/assets/js/')); 11 | } 12 | else { 13 | return gulp.src(global.paths.configJSSample) 14 | .pipe($.rename('FUSION_CONFIG.js')) 15 | .pipe(gulp.dest('./')) 16 | .pipe(gulp.dest('./build/assets/js/')); 17 | } 18 | }); 19 | 20 | // Copies your app's page templates and generates URLs for them 21 | gulp.task('copy:configSample', function() { 22 | if(!fs.existsSync(global.paths.configJS[0])){ 23 | return gulp.src(global.paths.configJSSample).pipe($.rename('FUSION_CONFIG.js')).pipe(gulp.dest('./')); 24 | } 25 | else{ 26 | return false; 27 | } 28 | }); 29 | 30 | // gulp.task('writeBuildConfig', function(){ 31 | // fs.readFile('./FUSION_CONFIG.js', 'utf8', function(err, data){ 32 | // if (err) { 33 | // return console.log(err); 34 | // } 35 | // var file = 'var appConfig = (function(){\n var ' + data + 'return appConfig;})();'; 36 | // fs.writeFile('./build/assets/js/fusion_config.js',file); 37 | // }); 38 | // }); 39 | 40 | gulp.task('writeDevConfig', function(){ 41 | fs.readFile('./FUSION_CONFIG.js', 'utf8', function(err, data){ 42 | if (err) { 43 | return console.log(err); 44 | } 45 | var file = 'var ' + data + 'module.exports = appConfig;'; 46 | var dir = './tmp'; 47 | if (!fs.existsSync(dir)){ 48 | fs.mkdirSync(dir); 49 | } 50 | fs.writeFile('./tmp/fusion_config.js',file); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /client/assets/js/services/AuthService.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular 5 | .module('lucidworksView.services.auth', ['lucidworksView.services.apiBase', 6 | 'lucidworksView.services.config' 7 | ]) 8 | .factory('AuthService', AuthService); 9 | 10 | function AuthService($q, $log, $http, $rootScope, ApiBase, ConfigService) { 11 | 'ngInject'; 12 | var config = ConfigService.config; 13 | var realmName = config.connection_realm; 14 | 15 | return { 16 | createSession: createSession, 17 | getSession: getSession, 18 | destroySession: destroySession 19 | }; 20 | 21 | ////////////// 22 | 23 | 24 | function createSession(username, password) { 25 | var deferred = $q.defer(); 26 | $http 27 | .post(ApiBase.getEndpoint() + 'api/session?realmName=' + realmName, { 28 | username: username, 29 | password: password 30 | }) 31 | .then(function (resp) { 32 | deferred.resolve(resp); 33 | }, function (err) { 34 | deferred.reject(err); 35 | }); 36 | 37 | return deferred.promise; 38 | } 39 | 40 | function getSession() { 41 | var deferred = $q.defer(); 42 | $http 43 | .get(ApiBase.getEndpoint() + 'api/session?realmName=' + realmName) 44 | .then(function (resp) { 45 | deferred.resolve(resp); 46 | }, function (err) { 47 | deferred.reject(err); 48 | }); 49 | return deferred.promise; 50 | } 51 | 52 | function destroySession() { 53 | var deferred = $q.defer(); 54 | $http 55 | .delete(ApiBase.getEndpoint() + 'api/session?realmName=' + realmName) 56 | .then(function (resp) { 57 | deferred.resolve(resp); 58 | }); 59 | 60 | return deferred.promise; 61 | } 62 | } 63 | })(); 64 | -------------------------------------------------------------------------------- /tests/assets/js/services/ConfigServiceTest.js: -------------------------------------------------------------------------------- 1 | /*global ngDescribe, describe, it, expect*/ 2 | ngDescribe({ 3 | name: 'ConfigService', 4 | modules: 'lucidworksView.services', 5 | inject: ['ConfigService'], 6 | tests: function(deps){ 7 | describe('specific little properties getter should be working', function(){ 8 | it('should make the right fusion url', function(){ 9 | deps.ConfigService.init({ 10 | host: 'http://localhost', 11 | port: '8764' 12 | }); 13 | expect(deps.ConfigService.getFusionUrl()).toEqual('http://localhost:8764/'); 14 | }); 15 | }); 16 | 17 | describe('getSpecificFields() should be working', function(){ 18 | it('with specific field getters', function(){ 19 | deps.ConfigService.init({ 20 | thumbnails_field: 'thumb_url' 21 | }); 22 | expect(deps.ConfigService.getFields.get('thumbnails_field')).toEqual('thumb_url'); 23 | }); 24 | 25 | it('and null when blank', function(){ 26 | deps.ConfigService.init({ 27 | thumbnails_field: '' 28 | }); 29 | expect(deps.ConfigService.getFields.get('thumbnails_field')).toEqual(null); 30 | }); 31 | }); 32 | 33 | describe('getAllFields() with existing config properties',function(){ 34 | it('should return proper value', function(){ 35 | deps.ConfigService.init({ 36 | thumbnails_field: 'thumb_url' 37 | }); 38 | expect(deps.ConfigService.getFields.all().thumbnails_field).toEqual('thumb_url'); 39 | }); 40 | 41 | it('should return nothing if it\'s blank and enabled', function(){ 42 | deps.ConfigService.init({ 43 | thumbnails_field: '' 44 | }); 45 | expect(deps.ConfigService.getFields.all().thumbnails_field).toEqual(undefined); 46 | }); 47 | }); 48 | } 49 | }); 50 | -------------------------------------------------------------------------------- /client/assets/components/facetRange/facetRange.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular 5 | .module('lucidworksView.components.facetRange', [ 6 | 'lucidworksView.services.config', 7 | 'foundation.core', 8 | 'lucidworksView.utils.queryBuilder' 9 | ]) 10 | .config(Config); 11 | 12 | function Config(QueryBuilderProvider){ 13 | 'ngInject'; 14 | 15 | /** 16 | * Transformers. 17 | * 18 | * These will transform the output of the query, when the query is created. 19 | */ 20 | 21 | // Register transformers because range facet fields can have funky URL syntax. 22 | QueryBuilderProvider.registerTransformer('keyValue', 'fq:range', fqFieldkeyValueTransformer); 23 | QueryBuilderProvider.registerTransformer('encode', 'fq:range', fqFieldEncode); 24 | QueryBuilderProvider.registerTransformer('preEncodeWrapper', 'fq:range', fqFieldPreEncodeWrapper); 25 | QueryBuilderProvider.registerTransformer('wrapper', 'fq:range', fqFieldWrapper); 26 | QueryBuilderProvider.registerTransformer('join', 'TO', fqJoiner); 27 | QueryBuilderProvider.registerTransformer('keyValue', 'TO', fqKeyValueBlank); 28 | 29 | function fqKeyValueBlank(key, value){ 30 | return value; 31 | } 32 | 33 | function fqJoiner(str, value){ 34 | return str + ' TO ' + value; 35 | } 36 | 37 | function fqFieldkeyValueTransformer(key, value) { 38 | var escapedKey = QueryBuilderProvider.escapeSpecialChars(key); 39 | return QueryBuilderProvider.keyValueString(escapedKey, value, ':'); 40 | } 41 | 42 | function fqFieldEncode(data){ 43 | return QueryBuilderProvider.encodeURIComponentPlus(data); 44 | } 45 | 46 | function fqFieldPreEncodeWrapper(data){ 47 | return data; 48 | } 49 | 50 | function fqFieldWrapper(data){ 51 | return '['+data+']'; 52 | } 53 | } 54 | })(); 55 | -------------------------------------------------------------------------------- /client/assets/js/services/QueryService.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | angular.module('lucidworksView.services.query', ['lucidworksView.services.config', 3 | 'lucidworksView.services.queryData' 4 | ]) 5 | .config(Config) 6 | .provider('QueryService', QueryService); 7 | 8 | function Config(OrwellProvider) { 9 | 'ngInject'; 10 | OrwellProvider.createObservable('query', {}); 11 | } 12 | 13 | function QueryService() { 14 | 15 | this.$get = $get; 16 | 17 | ///////////// 18 | 19 | function $get($log, ConfigService, Orwell, QueryDataService, URLService) { 20 | 'ngInject'; 21 | var queryObservable = Orwell.getObservable('query'), 22 | queryObject = ConfigService.config.default_query; 23 | 24 | activate(); 25 | 26 | queryObservable.addObserver(function (query) { 27 | QueryDataService.getQueryResults(query); 28 | }); 29 | 30 | return { 31 | setQuery: setQuery, 32 | getQueryObservable: getQueryObservable, 33 | getQueryObject: getQueryObject 34 | }; 35 | 36 | function activate() { 37 | queryObservable.setContent(ConfigService.config.default_query); 38 | } 39 | 40 | function getQueryObservable() { 41 | return queryObservable; 42 | } 43 | 44 | function getQueryObject() { 45 | return queryObservable.getContent(); 46 | } 47 | 48 | /** 49 | * Sets the query and sets off a new search via observable; 50 | */ 51 | function setQuery(query) { 52 | if (ConfigService.config.query_debug) { 53 | $log.debug('query', query); 54 | } 55 | queryObject = _.assign({}, queryObject, query, {rows: ConfigService.config.docs_per_page}); 56 | queryObservable.setContent(queryObject); 57 | URLService.setQueryToURLAndGo(queryObject); 58 | } 59 | 60 | } 61 | } 62 | })(); 63 | -------------------------------------------------------------------------------- /win64/getting-started-on-windows.md: -------------------------------------------------------------------------------- 1 | # Lucidworks View on Windows 2 | 3 | After downloading `Lucidworks-View-Installer.exe` and running it **as an administrator**, go to services, find the Lucidworks View service, right click and select start. 4 | 5 | Navigate to localhost:3000 on your preferred browser. 6 | 7 | To stop the service go to services, right click the Lucidworks View service and select stop 8 | 9 | To uninstall the service, run the `.\uninstall-service.cmd` **as an administrator**. 10 | 11 | ## Basic Configuration 12 | 13 | The first time you run the View service, `FUSION_CONFIG.sample.js` is copied to `FUSION_CONFIG.js`. Modify this file to configure View's basic options. Documentation about the configuration keys is included in the file. 14 | 15 | At a minimum, you _must_ configure the `collection` key to match the name of your Fusion collection. 16 | 17 | In a production environment, you must also configure `host` and `port` to point to the UI service of your Fusion deployment. The default is `localhost:8764` for development purposes. 18 | 19 | When the app is running with `.\view.cmd`, it reloads the configuration every time you save `FUSION_CONFIG.js`. You can modify the configuration and watch the app change in real time in your browser. 20 | 21 | ## Basic Customization 22 | 23 | The title and logo for your interface are configured in FUSION_CONFIG.js as `search_app_title` and `logo_location`. 24 | 25 | CSS options are configured in the files in `client\assets\scss`. 26 | 27 | Templates for various UI components are located in `client\assets\components`. 28 | 29 | Search results from different document types can use different templates. The `client\assets\components\document` directory contains templates for some common document types, plus default templates for all others. Data types correspond to Connectors in Fusion. See [Customizing Documents](docs/Customizing_Documents.md) for details about working with these. 30 | -------------------------------------------------------------------------------- /client/assets/components/sort/sort.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular 5 | .module('lucidworksView.components.sort', []) 6 | .directive('sort', sort); 7 | 8 | function sort() { 9 | 'ngInject'; 10 | var directive = { 11 | restrict: 'E', 12 | templateUrl: 'assets/components/sort/sort.html', 13 | scope: true, 14 | controller: Controller, 15 | controllerAs: 'vm', 16 | bindToController: {} 17 | }; 18 | 19 | return directive; 20 | 21 | 22 | } 23 | 24 | function Controller($scope, ConfigService, QueryService) { 25 | 'ngInject'; 26 | var vm = this; 27 | vm.switchSort = switchSort; 28 | 29 | activate(); 30 | 31 | ///////////// 32 | 33 | function activate() { 34 | createSortList(); 35 | 36 | $scope.$watch('vm.selectedSort', handleSelectedSortChange); 37 | } 38 | 39 | function createSortList(){ 40 | var sortOptions = [{label:'default sort', type:'default', order:'', active: true}]; 41 | _.forEach(ConfigService.config.sort_fields, function(value){ 42 | sortOptions.push({label: value, type: 'text', order: 'asc', active: false}); 43 | sortOptions.push({label: value, type: 'text', order: 'desc', active: false}); 44 | }); 45 | vm.sortOptions = sortOptions; 46 | vm.selectedSort = vm.sortOptions[0]; 47 | } 48 | 49 | function handleSelectedSortChange(newValue, oldValue){ 50 | if(newValue === oldValue) return; 51 | 52 | switchSort(newValue); 53 | } 54 | 55 | function switchSort(sort){ 56 | var query = QueryService.getQueryObject(); 57 | switch(sort.type) { 58 | case 'text': 59 | if(angular.isUndefined(query.sort)){ 60 | query.sort = sort.label+'%20'+sort.order; 61 | QueryService.setQuery(query); 62 | } 63 | break; 64 | default: 65 | delete query.sort; 66 | QueryService.setQuery(query); 67 | } 68 | } 69 | } 70 | })(); 71 | -------------------------------------------------------------------------------- /client/templates/home_main-content-frame.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 |
6 | 15 |
16 | 17 | 18 |
19 |
20 | 21 |
22 |
23 | Showing Results for "{{hc.lastQuery}}". 24 |
25 |
26 | 27 |

Your results will live here

28 |

Try searching any query above in the search box

29 |
30 | 31 |
32 | 33 |

There are no results for “{{hc.lastQuery}}

34 |

Try searching on a different term or using different facets to refine your search

35 |
36 |
37 | 38 | 39 | 40 |
41 |
42 | 43 |
44 | -------------------------------------------------------------------------------- /client/assets/components/document/document_web/document_web.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular 5 | .module('lucidworksView.components.document_web', ['lucidworksView.services.signals']) 6 | .directive('documentWeb', documentWeb); 7 | 8 | function documentWeb() { 9 | 'ngInject'; 10 | var directive = { 11 | restrict: 'EA', 12 | templateUrl: 'assets/components/document/document_web/document_web.html', 13 | scope: true, 14 | controller: Controller, 15 | controllerAs: 'vm', 16 | bindToController: { 17 | doc: '=', 18 | position: '=', 19 | highlight: '=' 20 | } 21 | }; 22 | 23 | return directive; 24 | 25 | } 26 | 27 | function Controller(DocumentService) { 28 | 'ngInject'; 29 | var vm = this; 30 | var templateFields = ['title', 'url', 'id', 'keywords', 'description', 'og_description', 'content', 'body']; 31 | vm.postSignal = postSignal; 32 | vm.getTemplateDisplayFieldName = getTemplateDisplayFieldName; 33 | 34 | activate(); 35 | 36 | function activate() { 37 | vm.doc = processDocument(vm.doc); 38 | } 39 | 40 | function processDocument(doc) { 41 | //set properties needed for display 42 | doc._templateDisplayFields = DocumentService.setTemplateDisplayFields(doc, templateFields); 43 | 44 | doc._templateDisplayFields._lw_id_decoded = DocumentService.decodeFieldValue(doc._templateDisplayFields, 'id'); 45 | doc._templateDisplayFields._lw_url_decoded = DocumentService.decodeFieldValue(doc._templateDisplayFields, 'url'); 46 | 47 | //set properties needed for signals 48 | doc._signals = DocumentService.setSignalsProperties(doc, vm.position); 49 | 50 | return doc; 51 | } 52 | 53 | function postSignal(options){ 54 | DocumentService.postSignal(vm.doc._signals, options); 55 | } 56 | 57 | function getTemplateDisplayFieldName(field){ 58 | return DocumentService.getTemplateDisplayFieldName(vm.doc, field); 59 | } 60 | } 61 | })(); 62 | -------------------------------------------------------------------------------- /client/assets/scss/_fonts.scss: -------------------------------------------------------------------------------- 1 | /* Webfont: Lato-Bold */ 2 | @font-face { 3 | font-family: 'LatoWebBold'; 4 | src: url('../webfonts/Lato-Bold.eot'); /* IE9 Compat Modes */ 5 | src: url('../webfonts/Lato-Bold.woff2') format('woff2'), /* Modern Browsers */ 6 | url('../webfonts/Lato-Bold.woff') format('woff'), /* Modern Browsers */ 7 | url('../webfonts/Lato-Bold.ttf') format('truetype'); 8 | font-style: normal; 9 | font-weight: normal; 10 | text-rendering: optimizeLegibility; 11 | } 12 | 13 | /* Webfont: Lato-BoldItalic */ 14 | @font-face { 15 | font-family: 'LatoWebBold'; 16 | src: url('../webfonts/Lato-BoldItalic.eot'); /* IE9 Compat Modes */ 17 | src: url('../webfonts/Lato-BoldItalic.woff2') format('woff2'), /* Modern Browsers */ 18 | url('../webfonts/Lato-BoldItalic.woff') format('woff'), /* Modern Browsers */ 19 | url('../webfonts/Lato-BoldItalic.ttf') format('truetype'); 20 | font-style: italic; 21 | font-weight: normal; 22 | text-rendering: optimizeLegibility; 23 | } 24 | 25 | /* Webfont: Lato-Regular */ 26 | @font-face { 27 | font-family: 'LatoWeb'; 28 | src: url('../webfonts/Lato-Regular.eot'); /* IE9 Compat Modes */ 29 | src: url('../webfonts/Lato-Regular.woff2') format('woff2'), /* Modern Browsers */ 30 | url('../webfonts/Lato-Regular.woff') format('woff'), /* Modern Browsers */ 31 | url('../webfonts/Lato-Regular.ttf') format('truetype'); 32 | font-style: normal; 33 | font-weight: normal; 34 | text-rendering: optimizeLegibility; 35 | } 36 | 37 | /* Webfont: Lato-Italic */ 38 | @font-face { 39 | font-family: 'LatoWeb'; 40 | src: url('../webfonts/Lato-Italic.eot'); /* IE9 Compat Modes */ 41 | src: url('../webfonts/Lato-Italic.woff2') format('woff2'), /* Modern Browsers */ 42 | url('../webfonts/Lato-Italic.woff') format('woff'), /* Modern Browsers */ 43 | url('../webfonts/Lato-Italic.ttf') format('truetype'); 44 | font-style: italic; 45 | font-weight: normal; 46 | text-rendering: optimizeLegibility; 47 | } 48 | -------------------------------------------------------------------------------- /client/assets/components/document/document_jira/contentTypes/jiraProject.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | 4 | 5 | 6 |

7 | 8 |
9 |

10 | 12 |

13 |
14 |
15 | 17 |
18 |
19 | 20 | 21 | 22 |
23 |
24 | Last Modified: 25 |
26 |
27 | Lead: 28 |
29 |
30 |
31 |
32 | -------------------------------------------------------------------------------- /client/assets/components/document/document_jira/contentTypes/jiraIssue.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |

4 | 5 | 6 | 7 |

8 | 9 |
10 |

11 | 12 |

13 | 14 |
15 |
16 | 18 |
19 |
20 | 21 | 22 | 23 |
24 |
Last Modified:
25 |
Assigned to:
26 |
27 |
28 |
29 | -------------------------------------------------------------------------------- /client/assets/components/document/document_slack/document_slack.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular 5 | .module('lucidworksView.components.document_slack', ['lucidworksView.services.signals']) 6 | .directive('documentSlack', documentSlack); 7 | 8 | function documentSlack() { 9 | 'ngInject'; 10 | var directive = { 11 | restrict: 'EA', 12 | templateUrl: 'assets/components/document/document_slack/document_slack.html', 13 | scope: true, 14 | controller: Controller, 15 | controllerAs: 'vm', 16 | bindToController: { 17 | doc: '=', 18 | position: '=', 19 | highlight: '=' 20 | } 21 | }; 22 | 23 | return directive; 24 | 25 | } 26 | 27 | 28 | function Controller($filter, DocumentService) { 29 | 'ngInject'; 30 | var vm = this; 31 | var templateFields = ['text', 'channel', 'user', 'timestamp', 'id']; 32 | vm.postSignal = postSignal; 33 | vm.getTemplateDisplayFieldName = getTemplateDisplayFieldName; 34 | 35 | activate(); 36 | 37 | function activate() { 38 | vm.doc = processDocument(vm.doc); 39 | } 40 | 41 | function processDocument(doc) { 42 | //set properties needed for display 43 | doc._templateDisplayFields = DocumentService.setTemplateDisplayFields(doc, templateFields); 44 | 45 | doc._templateDisplayFields.timestamp_Formatted = $filter('date')(vm.doc._templateDisplayFields.timestamp, 'M/d/yy h:mm:ss a'); 46 | // For multivalued fields 47 | doc._templateDisplayFields.text = _.isArray(doc._templateDisplayFields.text) ? _.join(doc._templateDisplayFields.text, ' ') : doc._templateDisplayFields.text; 48 | doc._templateDisplayFields._lw_id_decoded = DocumentService.decodeFieldValue(doc._templateDisplayFields, 'id'); 49 | 50 | //set properties needed for signals 51 | doc._signals = DocumentService.setSignalsProperties(doc, vm.position); 52 | 53 | return doc; 54 | } 55 | 56 | function postSignal(options){ 57 | DocumentService.postSignal(vm.doc._signals, options); 58 | } 59 | 60 | function getTemplateDisplayFieldName(field){ 61 | return DocumentService.getTemplateDisplayFieldName(vm.doc, field); 62 | } 63 | } 64 | })(); 65 | -------------------------------------------------------------------------------- /win64/installer/Lucidworks View Windows Installer Creation Guide.md: -------------------------------------------------------------------------------- 1 | # Lucidworks View Windows Installer Creation Guide 2 | 3 | ## How to build the lucidworks view windows installer: 4 | 5 | Do the following steps from a windows box 6 | 7 | 1. Pull the latest changes from lucidworks-view repository and build the tar.gz file from Linux with option `--buildTarget=win64`. (See the README.md for build instructions on how to build lucidworks-view tar ball or download the latest tar at ) 8 | 9 | 2. Move the tar build on Linux and Untar on Windows to a directory we will refer to as **VIEW_HOME**. 10 | 11 | **Note:** Run your extraction utility as an administrator in order to properly keep the symlinks intact. You can do this by opening the extraction utility (7zip) by right clicking on the menu icon in programs and selecting run as administrator. 12 | 13 | 3. Get latest version of **inno setup** installed 14 | 15 | 4. Launch **VIEW_HOME**\installer\create-installer.cmd 16 | - This will create the Installer `Lucidworks-View-Installer.exe` in **VIEW_HOME**\installer\Output directory. 17 | 18 | ## How to use the Windows Installer: 19 | 20 | * Launch the `Lucidworks-View-Installer.exe` **as an administrator**. 21 | 22 | **Note:** At the end of the installer, it attempts to run npm installs. If the install cmd script closes too fast, you probably didn't have admin permissions and it abruptly failed. Right click on the install.cmd and run as administrator if that happens. 23 | 24 | ## How to Uninstall the Lucidworks-view service 25 | 26 | * To uninstall the service simply run the **VIEW_HOME**\uninstall-service.cmd as administrator 27 | 28 | ## Common issues: 29 | 30 | - File could not be access, file in use 31 | 32 | - Or you have one of the installer files open with a CMD or locked somewhere. Close it and try again. 33 | 34 | - Services didn't install. 35 | 36 | - You probably forgot to Run installer "as administrator" 37 | 38 | - Services won't start. 39 | 40 | - Did you enter correct password when prompted for the user that is launch the service? 41 | 42 | - Don't forget windows domain if needed. If my domain is **CORPORATE** then **"CORPORATE\nicholas"** 43 | -------------------------------------------------------------------------------- /client/assets/js/app.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular 5 | .module('lucidworksView', [ 6 | 'ui.router', 7 | 'ngAnimate', 8 | 'ngSanitize', 9 | 10 | // Foundation 11 | 'foundation', 12 | 'foundation.dynamicRouting', 13 | 'foundation.dynamicRouting.animations', 14 | 15 | // Libraries 16 | 'ngOrwell', 17 | 'rison', 18 | 'MassAutoComplete', 19 | 20 | // Fusion Seed App 21 | 'lucidworksView.components', 22 | 'lucidworksView.services', 23 | 'lucidworksView.controllers' 24 | ]) 25 | .constant('_', window._) //eslint-disable-line 26 | .config(config) 27 | .run(run); 28 | 29 | /** 30 | * Main app config 31 | * 32 | * @param {Provider} $urlRouterProvider Provider for url 33 | * @param {Provider} $httpProvider Provider for http 34 | * @param {Provider} $locationProvider Provider for location 35 | * @param {Provider} ApiBaseProvider Provider for ApiBase 36 | * @param {Provider} ConfigServiceProvider Provider for ConfigService 37 | */ 38 | function config($urlRouterProvider, $httpProvider, $locationProvider, ApiBaseProvider, 39 | ConfigServiceProvider, $windowProvider) { 40 | 'ngInject'; 41 | $urlRouterProvider.otherwise('/search'); 42 | $httpProvider.interceptors.push('AuthInterceptor'); 43 | $httpProvider.defaults['withCredentials'] = true; //eslint-disable-line 44 | 45 | $locationProvider.html5Mode({ 46 | enabled: true, 47 | requireBase: false 48 | }); 49 | 50 | $locationProvider.hashPrefix('!'); 51 | // If using a proxy use the same url. 52 | if (ConfigServiceProvider.config.use_proxy) { 53 | var $window = $windowProvider.$get(); 54 | ApiBaseProvider.setEndpoint($window.location.protocol + '//' + $window.location.host + 55 | '/'); 56 | } else { 57 | ApiBaseProvider.setEndpoint(ConfigServiceProvider.getFusionUrl()); 58 | } 59 | } 60 | 61 | /** 62 | * Main app run time 63 | * 64 | * @param {Service} $document Document service 65 | */ 66 | function run($document, $rootScope, ConfigService) { 67 | 'ngInject'; 68 | $rootScope.title = ConfigService.config.search_app_title; 69 | } 70 | })(); 71 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | /*global module */ 2 | // Karma configuration 3 | // Generated on Tue Jan 19 2016 21:47:15 GMT+0530 (IST) 4 | 5 | module.exports = function(config) { 6 | config.set({ 7 | 8 | // base path that will be used to resolve all patterns (eg. files, exclude) 9 | basePath: '', 10 | 11 | 12 | // frameworks to use 13 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 14 | frameworks: ['jasmine'], 15 | 16 | 17 | // list of files / patterns to load in the browser 18 | files: [ 19 | 'build/assets/js/foundation.js', 20 | 'build/assets/js/templates.js', 21 | 'build/assets/js/routes.js', 22 | // 'build/assets/js/FUSION_CONFIG.js', 23 | 'bower_components/angular-mocks/angular-mocks.js', 24 | 'build/assets/js/app.js', 25 | 'node_modules/ng-describe/dist/ng-describe.js', 26 | 'tests/test_config.js', 27 | 'tests/**/*Test.js' 28 | ], 29 | 30 | // list of files to exclude 31 | exclude: [ 32 | ], 33 | 34 | 35 | // preprocess matching files before serving them to the browser 36 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 37 | preprocessors: { 38 | }, 39 | 40 | 41 | // test results reporter to use 42 | // possible values: 'dots', 'progress' 43 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 44 | reporters: ['progress'], 45 | 46 | 47 | // web server port 48 | port: 9876, 49 | 50 | 51 | // enable / disable colors in the output (reporters and logs) 52 | colors: true, 53 | 54 | 55 | // level of logging 56 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 57 | logLevel: config.LOG_INFO, 58 | 59 | 60 | // enable / disable watching file and executing tests whenever any file changes 61 | autoWatch: true, 62 | 63 | 64 | // start these browsers 65 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 66 | browsers: ['Chrome'], 67 | 68 | 69 | // Continuous Integration mode 70 | // if true, Karma captures browsers, runs the tests and exits 71 | singleRun: false, 72 | 73 | // Concurrency level 74 | // how many browser should be started simultaneous 75 | concurrency: Infinity 76 | }); 77 | }; 78 | -------------------------------------------------------------------------------- /win64/installer/inno-setup-installer.iss: -------------------------------------------------------------------------------- 1 | ; Script generated by the Inno Setup Script Wizard. 2 | ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! 3 | 4 | #define MyAppName "Lucidworks View" 5 | #define MyAppVersion "1.3.0" 6 | #define MyAppPublisher "Lucidworks" 7 | #define MyAppURL "https://www.lucidworks.com/products/view" 8 | #define MyAppExeName "view.cmd" 9 | 10 | [Setup] 11 | ; NOTE: The value of AppId uniquely identifies this application. 12 | ; Do not use the same AppId value in installers for other applications. 13 | ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) 14 | AppId={{088A54F9-BABA-4F09-A288-0F8BB4DD9E54} 15 | AppName={#MyAppName} 16 | AppVersion={#MyAppVersion} 17 | ;AppVerName={#MyAppName} {#MyAppVersion} 18 | AppPublisher={#MyAppPublisher} 19 | AppPublisherURL={#MyAppURL} 20 | AppSupportURL={#MyAppURL} 21 | AppUpdatesURL={#MyAppURL} 22 | DefaultDirName=\lucidworks-view 23 | DefaultGroupName={#MyAppName} 24 | DisableProgramGroupPage=yes 25 | OutputBaseFilename=Lucidworks-View-Installer 26 | SetupIconFile={#SourcePath}\Lucidworks-Glyph.ico 27 | Compression=lzma 28 | SolidCompression=yes 29 | 30 | [Registry] 31 | Root: HKLM; Subkey: "SYSTEM\CurrentControlSet\Control\Session Manager\Environment"; ValueType:expandsz; ValueName:"PATH"; ValueData:"{olddata};{app}\lib\nodejs"; Flags: preservestringtype 32 | Root: HKLM; Subkey: "SYSTEM\CurrentControlSet\Control\Session Manager\Environment"; ValueType:expandsz; ValueName:"PATH"; ValueData:"{olddata};{app}\node_modules\.bin"; Flags: preservestringtype 33 | 34 | [Languages] 35 | Name: "english"; MessagesFile: "compiler:Default.isl" 36 | 37 | [Files] 38 | Source: "{#SourcePath}\..\*"; Excludes: "\installer"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs 39 | ; NOTE: Don't use "Flags: ignoreversion" on any shared system files 40 | 41 | [Icons] 42 | Name: "{group}\Uninstall Lucidworks View"; Filename: "{uninstallexe}" 43 | 44 | [Code] 45 | procedure CurPageChanged(CurPageID: Integer); 46 | var 47 | ErrorCode : Integer ; 48 | begin 49 | if CurPageID = wpFinished then 50 | ShellExecAsOriginalUser('', ExpandConstant('{app}\install.cmd'), '', '',SW_SHOWNORMAL, ewNoWait, ErrorCode); 51 | end; 52 | 53 | [Run] 54 | Filename: "{app}\getting-started-on-windows.md"; Description: "View the README file"; Flags: postinstall shellexec skipifsilent 55 | -------------------------------------------------------------------------------- /gulp/serve.js: -------------------------------------------------------------------------------- 1 | /*eslint-env node*/ 2 | var log = require('connect-logger'); 3 | var argv = require('yargs').argv; 4 | var gulp = require('gulp'); 5 | var browserSync = require('browser-sync').create(); 6 | var historyFallback = require('connect-history-api-fallback'); 7 | var proxyMiddleware = require('http-proxy-middleware'); 8 | 9 | // Static Server + watching build and live reload accross all the browsers 10 | gulp.task('browsersync', ['build'], function() { 11 | var fusionConfig = require('../tmp/fusion_config'); 12 | var openPath = getOpenPath(); 13 | 14 | var proxyConfig = { 15 | target: fusionConfig.host+':'+fusionConfig.port 16 | }; 17 | 18 | // Allow self signed proxys to pass through with setting. 19 | if(fusionConfig.proxy_allow_self_signed_cert === true) { 20 | proxyConfig['secure'] = false; 21 | } 22 | 23 | // build middleware. 24 | var middleware = [ 25 | log(), 26 | proxyMiddleware('/api', proxyConfig), 27 | historyFallback({ index: '/' + openPath + '/index.html' }) 28 | ]; 29 | 30 | var browserSyncConfig = { 31 | baseDir: './build/', 32 | middleware: middleware 33 | }; 34 | 35 | if(fusionConfig.use_https === true){ 36 | browserSyncConfig['https'] = true; 37 | if(fusionConfig.hasOwnProperty('https') && fusionConfig.https.hasOwnProperty('key') && fusionConfig.https.hasOwnProperty('cert')){ 38 | browserSyncConfig['https'] = { 39 | key: fusionConfig.https.key, 40 | cert: fusionConfig.https.cert 41 | }; 42 | } 43 | } 44 | 45 | var serverPort = 3000; 46 | if(fusionConfig.server_port && fusionConfig.server_port !== false){ 47 | serverPort = fusionConfig.server_port; 48 | } 49 | 50 | browserSync.init({ 51 | server: browserSyncConfig, 52 | ghostMode: false, 53 | ui: false, 54 | port: serverPort 55 | }); 56 | 57 | // gulp.watch("app/scss/*.scss", ['sass']); 58 | // gulp.watch("app/*.html").on('change', browserSync.reload); 59 | }); 60 | 61 | //Reloads all the browsers 62 | gulp.task('reloadBrowsers', function(cb){ 63 | browserSync.reload(); 64 | //callback so sequences are aware when this is done 65 | cb(); 66 | }); 67 | 68 | gulp.task('serve', ['browsersync', 'watch']); 69 | 70 | gulp.task('default', ['browsersync', 'watch']); 71 | 72 | function getOpenPath() { 73 | var src = argv.open || ''; 74 | if (!src) { 75 | return '.'; 76 | } 77 | return src; 78 | } 79 | -------------------------------------------------------------------------------- /client/assets/components/searchBox/_searchBox.scss: -------------------------------------------------------------------------------- 1 | /* 2 | SEARCH BOX 3 | ---------- 4 | 5 | The search box lets you search content, and see typeahead results. 6 | */ 7 | 8 | /// @Lucidworks-view.settings 9 | // Search-box 10 | $search-box-typeahead-border: 1px solid #ddd; 11 | $search-box-typeahead-background: $body-background; 12 | $search-box-typeahead-text-color: grayscale($primary-color); 13 | $search-box-typeahead-selected-background: $gray-light; 14 | $search-box-typeahead-highlight-color: $success-color; 15 | $search-box-empty-content: ''; 16 | 17 | @include exports(search-box) { 18 | 19 | .search-box { 20 | > span {position:relative;display: block;} 21 | .query-input { 22 | margin-bottom: 0px; 23 | color: $search-box-typeahead-text-color; 24 | } 25 | .ac-container {z-index: 1000;backface-visibility: hidden;transform: translate3d(0, 0, 0);} 26 | 27 | .ac-menu,.no-results-message { 28 | background: $search-box-typeahead-background; 29 | border: $search-box-typeahead-border; 30 | color: $search-box-typeahead-text-color; 31 | list-style-type: none; 32 | margin: 0; 33 | padding: 4px; 34 | text-align:left; 35 | } 36 | .no-results-message { 37 | font-size:0.9rem; 38 | position:absolute; 39 | top:100%; 40 | width: 100%; 41 | padding: 8px; 42 | font-style: italic; 43 | } 44 | 45 | .ac-menu-item.ac-state-focus a {background: $search-box-typeahead-selected-background;} 46 | .ac-menu-item a { 47 | padding: .5rem 0 .5rem .5rem; 48 | color:inherit; 49 | display: block; 50 | position:relative; 51 | //extra element for title tag 52 | .title-wrapper { 53 | padding-right:1.5rem; 54 | white-space: nowrap; 55 | overflow: hidden; 56 | text-overflow: ellipsis; 57 | } 58 | &:hover {color: $search-box-typeahead-highlight-color;background: $search-box-typeahead-selected-background;} 59 | &:active {background: $search-box-typeahead-selected-background;} 60 | &:after { 61 | content:$search-box-empty-content; 62 | position:absolute; 63 | right:0; 64 | top:8px; 65 | background-image: url('../img/iconic/expand.svg'); 66 | transform: scaleX(-1); 67 | background-repeat: no-repeat; 68 | width: 1.5rem; 69 | height: 1.3rem; 70 | } 71 | .highlight { 72 | color: $search-box-typeahead-highlight-color; 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /client/assets/js/services/DocumentService.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular 5 | .module('lucidworksView.services.document', []) 6 | .factory('DocumentService', DocumentService); 7 | 8 | function DocumentService(PaginateService, SignalsService) { 9 | 'ngInject'; 10 | 11 | return { 12 | setSignalsProperties: setSignalsProperties, 13 | setTemplateDisplayFields: setTemplateDisplayFields, 14 | postSignal: postSignal, 15 | getTemplateDisplayFieldName: getTemplateDisplayFieldName, 16 | decodeFieldValue: decodeFieldValue 17 | }; 18 | 19 | function setSignalsProperties(doc,position) { 20 | var signalsProperties = {}; 21 | signalsProperties.signals_doc_id = SignalsService.getSignalsDocumentId(doc); 22 | signalsProperties.position = position; 23 | signalsProperties.page = PaginateService.getNormalizedCurrentPage(); 24 | 25 | return signalsProperties; 26 | } 27 | 28 | function setTemplateDisplayFields(doc,fieldsList) { 29 | var displayFields = {}; 30 | //TODO: refactor this 31 | _.forEach(fieldsList,function(field) { 32 | var fieldValue = _.get(doc,field+'_s') 33 | || _.get(doc,field+'_l') 34 | || _.get(doc,field+'_dt') 35 | || _.get(doc,field+'_t') 36 | || _.get(doc,field); 37 | displayFields[field] = _.isArray(fieldValue) ? fieldValue[0] : fieldValue; 38 | }); 39 | 40 | return displayFields; 41 | } 42 | 43 | function postSignal(_signals, options){ 44 | var paramsObj = { 45 | params: { 46 | position: _signals.position, 47 | page: _signals.page 48 | } 49 | }; 50 | _.defaultsDeep(paramsObj, options); 51 | SignalsService.postClickSignal(_signals.signals_doc_id, paramsObj); 52 | } 53 | 54 | function getTemplateDisplayFieldName(doc, field){ 55 | if(_.has(doc, field+'_s')){ 56 | return field + '_s'; 57 | } else if (_.has(doc, field+'_l')){ 58 | return field + '_l'; 59 | } else if (_.has(doc, field+'_dt')){ 60 | return field + '_dt'; 61 | } else if (_.has(doc, field+'_t')){ 62 | return field + '_t'; 63 | } 64 | return field; 65 | } 66 | 67 | function decodeFieldValue(doc, field){ 68 | if(_.has(doc, field) && _.isArray(doc[field])){ 69 | var x = _.has(doc, field + '[0]') ? decodeURIComponent(doc[field]) : doc[field]; 70 | return x; 71 | } 72 | var y = doc[field] ? decodeURIComponent(doc[field]) : doc[field]; 73 | return y; 74 | } 75 | } 76 | })(); 77 | -------------------------------------------------------------------------------- /client/assets/components/document/document_default/document_default.html: -------------------------------------------------------------------------------- 1 | 29 |
30 |
31 |
32 | 33 |
34 |
35 |
36 | 37 | 38 |

39 | 40 |

41 | 42 |

43 | 44 |

45 |

46 | 47 |

48 | 49 |

50 | 51 | {{::field}}:   52 | 53 | 54 | 55 |

56 |
57 |
58 | -------------------------------------------------------------------------------- /client/assets/components/field/field.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | angular.module('lucidworksView.components.field', ['lucidworksView.services.config', 3 | 'lucidworksView.utils.docs' 4 | ]) 5 | .directive('field', fieldItem); 6 | 7 | 8 | function fieldItem() { 9 | 'ngInject'; 10 | return { 11 | restrict: 'EA', 12 | templateUrl: 'assets/components/field/field.html', 13 | scope: true, 14 | controller: Controller, 15 | controllerAs: 'fc', 16 | bindToController: { 17 | formattingHandler: '=', 18 | value: '=', 19 | highlight: '=', 20 | hkey: '@', //The Highlight key, used to look up the proper highlighting snippet by name. 21 | maxlength: '=' 22 | }, 23 | replace: true 24 | }; 25 | } 26 | 27 | function Controller($log, $sanitize) { 28 | 'ngInject'; 29 | var fc = this; 30 | fc.limit = false; 31 | fc.limitValue = fc.maxlength;//keep track of the limitValue, as the user can toggle this w/ the Read More/Less toggle, but not the max length value 32 | fc.showMore = false; 33 | fc.totalLength = -1; 34 | fc.toggleRead = toggleRead; 35 | activate(); 36 | 37 | /////////// 38 | 39 | function activate() { 40 | fc.value = processField(fc.value, fc.hkey, fc.highlight, fc.maxlength); 41 | } 42 | 43 | function toggleRead(){ 44 | if (fc.limit) {//this only applies when we have more than the limitValue chars 45 | if (!fc.showMore) { 46 | //toggle to true, meaning user wants to see more 47 | fc.showMore = true; 48 | fc.limitValue = fc.totalLength; 49 | } else { 50 | fc.showMore = false; 51 | fc.limitValue = fc.maxlength; 52 | } 53 | } 54 | } 55 | 56 | function processField(field, highlightKey, highlight, maxlength) { 57 | var result = $sanitize(_.escape(field)); 58 | var hasHighlight = false; 59 | if (highlight && Object.keys(highlight).length > 0) { 60 | if (highlight[highlightKey]) { 61 | result = highlight[highlightKey]; 62 | } 63 | } 64 | // Only shorten if not highlighting, since highlighing in solr can control 65 | // snippet size there and b/c we have a Trusted object and not a plain 66 | // old string. 67 | 68 | // If field is multivalued, join the items before trimming 69 | result = _.isArray(result)?_.join(result, ' '):result; 70 | 71 | if (hasHighlight === false && result && result.length > maxlength) { 72 | // Mark this as being trimmed, but don't actually physically trim it 73 | fc.limit = true; 74 | } 75 | fc.totalLength = result.length; 76 | if (fc.formattingHandler){ 77 | result = fc.formattingHandler(result); 78 | } 79 | return result; 80 | } 81 | } 82 | })(); 83 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lucidworks-view", 3 | "version": "1.4.0", 4 | "description": "Lucidworks View: Create custom user experiences for your Fusion-powered apps.", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/lucidworks/lucidworks-view.git" 8 | }, 9 | "keywords": [ 10 | "lucidworks", 11 | "fusion", 12 | "angular", 13 | "search" 14 | ], 15 | "author": "Joshua Ellinger ", 16 | "contributors": [ 17 | "Joshua Ellinger ", 18 | "Arjit Dasgupta ", 19 | "Ariel Isaacs ", 20 | "Josh Lipps " 21 | ], 22 | "license": "Apache-2.0", 23 | "bugs": { 24 | "url": "https://github.com/lucidworks/lucidworks-view/issues" 25 | }, 26 | "changelog": "https://github.com/lucidworks/lucidworks-view/blob/master/CHANGELOG.md", 27 | "homepage": "https://lucidworks.com/products/view", 28 | "scripts": { 29 | "start": "./node_modules/gulp/bin/gulp.js --production", 30 | "prestart-dev": "npm install", 31 | "start-dev": "./node_modules/gulp/bin/gulp.js", 32 | "build": "./node_modules/gulp/bin/gulp.js build --production", 33 | "test": "./node_modules/karma/bin/karma start karma.conf.js" 34 | }, 35 | "devDependencies": { 36 | "bower": "1.8.8", 37 | "browser-sync": "^2.11.0", 38 | "connect-history-api-fallback": "^1.1.0", 39 | "connect-logger": "0.0.1", 40 | "eslint": "^3.3.1", 41 | "eslint-config-angular": "^0.5.0", 42 | "eslint-plugin-angular": "^1.3.1", 43 | "front-router": "^1.0.0", 44 | "gulp": "^3.9.0", 45 | "gulp-angular-templatecache": "^1.8.0", 46 | "gulp-autoprefixer": "^3.1.0", 47 | "gulp-chmod": "^1.3.0", 48 | "gulp-concat": "^2.6.0", 49 | "gulp-cssnano": "^2.1.2", 50 | "gulp-directive-replace": "^0.2.0", 51 | "gulp-eslint": "^3.0.1", 52 | "gulp-htmlmin": "^1.3.0", 53 | "gulp-if": "^2.0.0", 54 | "gulp-json-editor": "^2.2.1", 55 | "gulp-load-plugins": "^1.1.0", 56 | "gulp-ng-annotate": "^1.1.0", 57 | "gulp-ng-html2js": "^0.2.0", 58 | "gulp-plumber": "^1.0.1", 59 | "gulp-rename": "^1.2.2", 60 | "gulp-requirejs": "^0.1.3", 61 | "gulp-sass": "^2.0.4", 62 | "gulp-shell": "^0.5.2", 63 | "gulp-sourcemaps": "^1.6.0", 64 | "gulp-uglify": "^1.2.0", 65 | "gulp-webserver": "^0.9.1", 66 | "http-proxy-middleware": "^0.17.3", 67 | "jasmine-core": "^2.4.1", 68 | "karma": "^0.13.19", 69 | "karma-chrome-launcher": "^0.2.2", 70 | "karma-jasmine": "^0.3.6", 71 | "ng-describe": "^1.6.0", 72 | "require-dir": "^0.3.0", 73 | "rimraf": "^2.4.1", 74 | "run-sequence": "^1.1.1", 75 | "sass-lint": "^1.5.1", 76 | "yargs": "^6.6.0" 77 | }, 78 | "engine": "node >=5.2.0" 79 | } 80 | -------------------------------------------------------------------------------- /tests/assets/js/services/QueryDataServiceTest.js: -------------------------------------------------------------------------------- 1 | /*global ngDescribe, describe, it, expect*/ 2 | ngDescribe({ 3 | name: 'QueryService', 4 | modules: 'lucidworksView.services', 5 | inject: ['ConfigService', 'QueryDataService', 'ApiBase', '$httpBackend'], 6 | http:{ 7 | get:{ 8 | 'templates/home.html': '' 9 | } 10 | }, 11 | tests: function(deps){ 12 | beforeEach(function(){ 13 | deps.ConfigService.init(window.appConfig); 14 | deps.ApiBase.setEndpoint(deps.ConfigService.getFusionUrl()); 15 | }); 16 | 17 | describe('it should make the right call', function(){ 18 | it('with the right query', function(){ 19 | deps.$httpBackend.expectGET('http://localhost:8764/api/apollo/collections/MyCollection/query-profiles/default/select?q=hello&wt=json').respond({'key':'value'}); 20 | deps.QueryDataService.getQueryResults({ 21 | q: 'hello', 22 | wt: 'json' 23 | }).then(function(resp){ 24 | expect(resp).toEqual({'key':'value'}); 25 | }); 26 | deps.step(); 27 | }); 28 | 29 | it('with the right query with multiple query params', function(){ 30 | deps.$httpBackend.expectGET('http://localhost:8764/api/apollo/collections/MyCollection/query-profiles/default/select?q=hello&fq=hello2u2&wt=json').respond({'key':'value'}); 31 | deps.QueryDataService.getQueryResults({ 32 | q: 'hello', 33 | fq: 'hello2u2', 34 | wt: 'json' 35 | }).then(function(resp){ 36 | expect(resp).toEqual({'key':'value'}); 37 | }); 38 | deps.step(); 39 | }); 40 | }); 41 | 42 | describe('it should make the right call with query pipelines as well', function(){ 43 | it('with the right query', function(){ 44 | deps.ConfigService.init({use_query_profile: false}); 45 | deps.$httpBackend.expectGET('http://localhost:8764/api/apollo/query-pipelines/default/collections/MyCollection/select?q=hello&wt=json').respond({'key':'value'}); 46 | deps.QueryDataService.getQueryResults({ 47 | q: 'hello', 48 | wt: 'json' 49 | }).then(function(resp){ 50 | expect(resp).toEqual({'key':'value'}); 51 | }); 52 | deps.step(); 53 | }); 54 | 55 | it('with the right query with multiple query params', function(){ 56 | deps.ConfigService.init({use_query_profile: false}); 57 | deps.$httpBackend.expectGET('http://localhost:8764/api/apollo/query-pipelines/default/collections/MyCollection/select?q=hello&fq=hello2u2&wt=json').respond({'key':'value'}); 58 | deps.QueryDataService.getQueryResults({ 59 | q: 'hello', 60 | fq: 'hello2u2', 61 | wt: 'json' 62 | }).then(function(resp){ 63 | expect(resp).toEqual({'key':'value'}); 64 | }); 65 | deps.step(); 66 | }); 67 | }); 68 | } 69 | }); 70 | -------------------------------------------------------------------------------- /gulp/build.js: -------------------------------------------------------------------------------- 1 | /*eslint-env node*/ 2 | /*global global*/ 3 | /*eslint no-console:0*/ 4 | 5 | var $ = require('gulp-load-plugins')(); 6 | var gulp = require('gulp'); 7 | var router = require('front-router'); 8 | var sequence = require('run-sequence'); 9 | 10 | // Builds your entire app once, without starting a server 11 | gulp.task('build', function(cb) { 12 | sequence('clean', 'copy:config', 'writeDevConfig', ['copy', 'copy:foundation', 'sass', 'uglify'], 'copy:templates', cb); 13 | }); 14 | 15 | // Copies everything in the client folder except templates, Sass, and JS 16 | gulp.task('copy', function() { 17 | return gulp.src(global.paths.assets, { 18 | base: './client/' 19 | }) 20 | .pipe(gulp.dest('./build')); 21 | }); 22 | 23 | // Copies your app's page templates and generates URLs for them 24 | gulp.task('copy:templates', function() { 25 | return gulp.src('./client/templates/**/*.html') 26 | .pipe(router({ 27 | path: 'build/assets/js/routes.js', 28 | root: 'client' 29 | })) 30 | .pipe(gulp.dest('./build/templates')); 31 | }); 32 | 33 | // Compiles the Foundation for Apps directive partials into a single JavaScript file 34 | gulp.task('copy:foundation', function(cb) { 35 | gulp.src('bower_components/foundation-apps/js/angular/components/**/*.html') 36 | .pipe($.ngHtml2js({ 37 | prefix: 'components/', 38 | moduleName: 'foundation', 39 | declareModule: false 40 | })) 41 | .pipe($.uglify()) 42 | .pipe($.concat('templates.js')) 43 | .pipe(gulp.dest('./build/assets/js')); 44 | 45 | // Iconic SVG icons 46 | gulp.src('./bower_components/foundation-apps/iconic/**/*') 47 | .pipe(gulp.dest('./build/assets/img/iconic/')); 48 | 49 | cb(); 50 | }); 51 | 52 | // Compiles and copies the Foundation for Apps JavaScript, as well as your app's custom JS 53 | gulp.task('uglify', ['uglify:foundation', 'uglify:app']); 54 | 55 | gulp.task('uglify:foundation', function() { 56 | var uglify = $.if(global.isProduction, $.uglify() 57 | .on('error', function (e) { 58 | console.log(e); 59 | })); 60 | 61 | return gulp.src(global.paths.foundationJS) 62 | .pipe(uglify) 63 | .pipe($.concat('foundation.js')) 64 | .pipe(gulp.dest('./build/assets/js/')); 65 | }); 66 | 67 | gulp.task('uglify:app', function() { 68 | var uglify = $.if(global.isProduction, $.uglify() 69 | .on('error', function (e) { 70 | console.log(e); 71 | })), 72 | sourcemapsInit = $.if(!global.isProduction, $.sourcemaps.init()), 73 | sourcemapsWrite = $.if(!global.isProduction, $.sourcemaps.write('.')); 74 | 75 | return gulp.src(global.paths.appJS, { base: 'client' }) 76 | .pipe($.plumber()) 77 | .pipe(sourcemapsInit) 78 | .pipe($.ngAnnotate()) 79 | .pipe(uglify) 80 | .pipe($.directiveReplace({root: 'client'})) 81 | .pipe($.concat('app.js')) 82 | .pipe(sourcemapsWrite) 83 | .pipe(gulp.dest('./build/assets/js/')); 84 | }); 85 | -------------------------------------------------------------------------------- /docs/HowToUseQueryBuilder.md: -------------------------------------------------------------------------------- 1 | # How to use query builder 2 | 3 | Query builder creates query strings from an object. 4 | 5 | ## First level of a query object 6 | 7 | This is where you can define different options which will be joined 8 | together by standard query syntax. 9 | 10 | An object when passed through `objectToURLString` will return a string: 11 | 12 | * object: 13 | 14 | ```javascript 15 | {aa: 'abcd', bb: 'defg'} 16 | ``` 17 | 18 | * string: 19 | 20 | ``` 21 | aa=abcd&bb=defg 22 | ``` 23 | 24 | ## Second level of a query object and beyond 25 | 26 | Acceptable types: 27 | 28 | * string 29 | 30 | Will be concatenated to the query string. 31 | 32 | * array 33 | 34 | Each element is run through the reducer. 35 | 36 | * key value object 37 | 38 | A key/value pair that will be concatenated together. 39 | 40 | The values in a key/value pair can contain an array, a string, or even another key value object, allowing you to nest as many as you need to create your query. 41 | 42 | ## Transformers 43 | 44 | Transformers enable you to extend query builder by using different syntax to join or mutate data. 45 | 46 | Transformers are called on each step of a reduction of a key value object. Each step runs in order, transforming the data at each step: 47 | 48 | 1. `preEncodeWrapper` 49 | - Wraps the output before encode is run. 50 | - Ex: Used in `facetField` for adding inner quotes to a value. 51 | - default = no transformation 52 | 2. `encode` 53 | - encodes the data into another format. 54 | - Ex: Used in `facetField` to encode the value into a URI-encoded component. 55 | - default = no transformation 56 | 3. `wrapper` 57 | - Wraps the data after `encode` has been called. 58 | - Ex. In `facetField`, adds braces around a value. 59 | - default = no transformation 60 | 4. `keyValue` 61 | - Joins a key and a value. 62 | - Ex. In `facetField`, it joins a key and a value with `:`. 63 | - default = '=' 64 | 5. `join` 65 | - Joins the key/value to the rest of the query string. 66 | - Ex. In `facetField`, it joins a the key value with ''. 67 | - default = '' 68 | 69 | ### Writing your own transformers 70 | You can add transformers in a service or in a module configuration. Just inject `QueryBuilderProvider` or `QueryBuilder`. 71 | 72 | The `registerTransformer` function allows you to register any transformers you want for use. These are then used in a key/value object and are triggered via the `transformer` property of a key/value object. 73 | 74 | Here's an example of registering a transformer: 75 | 76 | ```javascript 77 | QueryBuilderProvider.registerTransformer('wrapper', 'fq:field', fqFieldWrapper); 78 | function fqFieldWrapper(data){ 79 | return '('+data+')'; 80 | } 81 | ``` 82 | 83 | Now every time an object has that transformer it will be output with that wrapper. 84 | 85 | ```javascript 86 | {fq: [{ 87 | key: 'name', 88 | value: 'value', 89 | transformer: 'fq:field' 90 | }]} 91 | ``` 92 | 93 | if the above object just had that one transformer it would produce the string. 94 | 95 | ``` 96 | fq=name(value) 97 | ``` 98 | -------------------------------------------------------------------------------- /client/assets/js/services/SignalService.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular 5 | .module('lucidworksView.services.signals', ['lucidworksView.services.apiBase', 6 | 'lucidworksView.services.config' 7 | ]) 8 | .factory('SignalsService', SignalsService); 9 | 10 | function SignalsService(ApiBase, ConfigService, $http, $q, QueryService, ClientStatsService) { 11 | 'ngInject'; 12 | var service = { 13 | postClickSignal: postClickSignal, 14 | postSignalData: postSignalData, 15 | getSignalsDocumentId: getSignalsDocumentId 16 | }; 17 | 18 | return service; 19 | 20 | /** 21 | * Helper method to document click events. 22 | * @param {string} docId The document id 23 | * @param {object} options An object containing parameter overrides and options. 24 | * - The object can be any parameter which will be passed through, including parameters. 25 | * Ex: {type: 'custom', params: {filterQueries: ['something']}} 26 | * @return {promise} 27 | */ 28 | function postClickSignal(docId, options) { 29 | var date = new Date(), 30 | data = { 31 | params: { 32 | docId: docId, 33 | head_field: ConfigService.config.head_field, 34 | language: ClientStatsService.getBrowserLanguage(), 35 | platform: ClientStatsService.getBrowserPlatform(), 36 | user_agent: ClientStatsService.getBrowserUserAgent(), 37 | user_name: ConfigService.getLoginCredentials().username || ConfigService.config.anonymous_access.username, 38 | query: QueryService.getQueryObject().q 39 | }, 40 | pipeline: ConfigService.config.signals_pipeline, 41 | timestamp: date.toISOString(), 42 | type: ConfigService.config.signal_type 43 | }; 44 | 45 | _.defaultsDeep(data, options); 46 | 47 | return postSignalData([data]); 48 | } 49 | 50 | //Use if you want to post a raw signal where you have control of every signal entry. See postClickSignal for an example data structure 51 | function postSignalData(data) { 52 | var deferred = $q.defer(); 53 | $http 54 | .post(ApiBase.getEndpoint() + 'api/apollo/signals/' + ConfigService.config.collection, 55 | data) 56 | .then(success) 57 | .catch(failure); 58 | 59 | function success(response) { 60 | deferred.resolve(response.data); 61 | } 62 | 63 | function failure(err) { 64 | deferred.reject(err.data); 65 | } 66 | 67 | return deferred.promise; 68 | } 69 | 70 | /** 71 | * Given a document return a signals document ID value. 72 | * @param {object} doc The document 73 | * @return {string|Number} The document ID value 74 | */ 75 | function getSignalsDocumentId(doc) { 76 | var documentIdField = ConfigService.config.signals_document_id; 77 | if (doc.hasOwnProperty(documentIdField)) { 78 | return doc[documentIdField]; 79 | } 80 | else { 81 | return null; 82 | } 83 | 84 | } 85 | } 86 | })(); 87 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## 1.4.0 - Ra Ra Rasputin's Disco Tiara 4 | **New Features** 5 | * Now works out of the box with Fusion 3 6 | * Added support for changing the default query via FUSION_CONFIG.js 7 | * Added support for date range facets type 8 | * Added support for simple grouped results 9 | * New loading state for range facets to prevent multiple selections while page is loading 10 | 11 | **Bug Fixes** 12 | * When redirected through login, inital page query will no longer be cleared 13 | * Linked URLS with parameters now click through correctly 14 | * Only show pagination arrows when pagination is possible 15 | * Fixed typeahead autocomplete user entry race condition 16 | 17 | **Package Updates** 18 | * Updated ESLint to version 3.3.1. 19 | * Updated angular-ui-router to version 0.4.2 20 | 21 | **Deprecated API functions** 22 | * URLService.setQuery() 23 | 24 | Instead of using `URLService.setQuery()` use `QueryService.setQuery()` 25 | > `URLService.setQuery()` will be removed in View 1.5.0. 26 | 27 | ## 1.3.0 - Pikachu's Invisible Tiara - August 5, 2016 28 | **New Features** 29 | * Added a Windows packaged build, you can now run View on Windows 30 | * Improved performance by minifying builds by default and turning off page change animations 31 | * You can now specify which port View runs on 32 | * Introduced developer mode, which allows you to develop with unminified build objects, just `npm run start-dev` 33 | 34 | **Bug Fixes** 35 | * Fixed a bug in `npm run build` command 36 | 37 | ## 1.2.0 - Cersei's Iron Tiara - June 30, 2016 38 | **New Features** 39 | * Now support range facet type 40 | * Added multi select faceting, meaning you can facet on multiple items within a single query 41 | * Added 'clear all' button to facet lists, clearing all selected facets 42 | * Improved signals now pass in more information including position, language, and platform 43 | * Enhanced signals service, adding additional functionality 44 | * Improved field display in templates by setting set max-length 45 | 46 | **Bug fixes** 47 | * Fix typeahead of a query profile or pipeline 48 | * Fixed field values: HTML entities are now properly truncated 49 | 50 | ## 1.1.0 - Tsarina Alexandra Tiara - May 4, 2016 51 | **New Features** 52 | * Highlighting support for fields when configured in Fusion 53 | * Grouped results are now displayed when configured in the Fusion pipeline 54 | * Add support for self signed certs with https on connections between Fusion and View 55 | * Add ability to use View with https enabled. (You can even use your own cert!) 56 | * Add context and instructions when starting via view.sh command 57 | * Improved signals to work with more complex inputs 58 | 59 | **Bug fixes** 60 | * Fixed npm start command: NPM start command now tracks changes to HTML files. Now you can change your HTML and templates with reckless abandon 61 | * Fixed landing pages: Landing are now unique 62 | 63 | ## 1.0.0 - Initial release (Paper Tiara) - April 6, 2016 64 | 65 | The first release of Lucidworks View 66 | 67 | Contains out of the box support for: 68 | 69 | Landing pages 70 | - Fusion Field facets 71 | - Document display templates 72 | - Slack, Twitter, JIRA, web, local file, default 73 | - Color and logo customization 74 | - Authentication 75 | - Signals 76 | - Typeahead 77 | - Sorting 78 | -------------------------------------------------------------------------------- /docs/Customizing_Documents.md: -------------------------------------------------------------------------------- 1 | # How to customize documents 2 | 3 | Lucidworks View lets you customize the display of different document types, which correspond to datasources in Fusion. It includes built-in templates to display some of the most common document types: 4 | 5 | - `document_file` (filesystem) 6 | - `document_jira` (repository) 7 | - `document_slack` (social) 8 | - `document_twitter` (social) 9 | - `document_web` (web) 10 | 11 | All templates are located in the ```client/assets/components/document``` folder. 12 | 13 | ## Default document display 14 | 15 | There's also a `document_default` template to display any document type that doesn't match the above. You can configure this template in `FUSION_CONFIG.js` to expose different fields, depending on your data. 16 | 17 | Open `FUSION_CONFIG.js` and scroll to this line to see the available options: 18 | 19 | ``` 20 | * Document_default display 21 | ``` 22 | 23 | ## Adding custom document types 24 | 25 | You can add new document types by 26 | 27 | 1. creating a new document component and 28 | 2. modifying the `getDocType` function to trigger your template. 29 | 30 | ### Creating a new document component 31 | 1. Copy one of the existing template directories and give it a name like `document_`. 32 | 1. Give the files in the new directory names that match the directory name, like this: 33 | 34 | ``` 35 | _document_.scss 36 | document_.html 37 | document_.js 38 | ``` 39 | 40 | See the `client/assets/components/document/document_EXAMPLE` directory for an example. 41 | 42 | 1. Customize the templates as needed. 43 | At a minimum, you _must_ modify the following values in `document_.js`: 44 | 45 | * `module` 46 | * `directive` 47 | * `templateURL` 48 | 49 | For more information about customizing Angular templates, see https://code.angularjs.org/1.4.8/docs/guide/templates. 50 | 51 | 1. Add the value of `module` to the list in ```client/assets/components/components.js```. 52 | 53 | 1. Add the value of `directive` to ```client/assets/components/documentList/documentList.html```. 54 | * You will need to add the value in 2 places in this file. Once in the first list, and once in the grouped results section, if you want the doc type to show up when you have regular and grouped results. 55 | 56 | ### Modifying the getDocType function 57 | 58 | The `getDocType` method specifies the data field that the app uses to select a template. The default is `_lw_data_source_type_s`, but you can add conditional statements to read additional fields. 59 | 60 | 1. Open ```client/assets/components/documentList/documentList.js``` and locate this section: 61 | 62 | ```javascript 63 | /** 64 | * Get the document type for the document. 65 | * @param {object} doc Document object 66 | * @return {string} Type of document 67 | */ 68 | function getDocType(doc){ 69 | // Change to your collection datasource type name 70 | // if(doc['_lw_data_source_s'] === 'MyDatasource-default'){ 71 | // return doc['_lw_data_source_s']; 72 | // } 73 | return doc['_lw_data_source_type_s']; 74 | } 75 | ``` 76 | 1. Uncomment the `if` statement. 77 | 1. Replace `_lw_data_source_s` with the name of a field from your data. 78 | 1. Replace `MyDatasource-default` with the name of your template. 79 | -------------------------------------------------------------------------------- /client/assets/components/document/document_web/document_web.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |

5 | 6 | 7 | 8 |

9 | 10 |

11 | 12 | 13 | 14 |

15 |

16 | 17 | 18 | 19 |

20 |

21 | 22 |

23 | 25 | 27 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 |

37 |
38 |
39 | -------------------------------------------------------------------------------- /client/assets/img/logo/lucidworks-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | Slice 2 8 | Created with Sketch. 9 | 10 | 12 | 15 | 17 | 20 | 22 | 25 | 27 | 29 | 32 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /client/assets/js/services/QueryDataService.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | angular 3 | .module('lucidworksView.services.queryData', [ 4 | 'lucidworksView.services.config', 5 | 'lucidworksView.services.apiBase', 6 | 'lucidworksView.utils.queryBuilder', 7 | 'ngOrwell' 8 | ]) 9 | .config(Config) 10 | .provider('QueryDataService', QueryDataService); 11 | 12 | 13 | function Config(OrwellProvider) { 14 | 'ngInject'; 15 | OrwellProvider.createObservable('queryResults', {}); 16 | } 17 | 18 | function QueryDataService() { 19 | 20 | this.$get = $get; 21 | 22 | ///////////// 23 | 24 | function $get($q, $http, ConfigService, ApiBase, Orwell, QueryBuilder) { 25 | 'ngInject'; 26 | var queryResultsObservable = Orwell.getObservable('queryResults'); 27 | return { 28 | getQueryResults: getQueryResults, 29 | getProfileEndpoint: getProfileEndpoint, 30 | getPipelineEndpoint: getPipelineEndpoint 31 | }; 32 | 33 | /** 34 | * Make a query to the query profiles endpoint 35 | * @param {object} query Should have all the query params, like 36 | * For select?q=query&fq=blah you need to pass in an object 37 | * {'q': 'query', 'fq': 'blah'} 38 | * @return {Promise} Promise that resolve with a Fusion response coming from Solr 39 | */ 40 | function getQueryResults(query) { 41 | var deferred = $q.defer(); 42 | 43 | var queryString = QueryBuilder.objectToURLString(query); 44 | 45 | var fullUrl = getQueryUrl(ConfigService.getIfQueryProfile()) + '?' + queryString; 46 | 47 | $http 48 | .get(fullUrl) 49 | .then(success) 50 | .catch(failure); 51 | 52 | function success(response) { 53 | // Set the content to populate the rest of the ui. 54 | queryResultsObservable.setContent(response.data); 55 | deferred.resolve(response.data); 56 | } 57 | 58 | function failure(err) { 59 | queryResultsObservable.setContent({ 60 | numFound: 0 61 | }); 62 | deferred.reject(err); 63 | } 64 | 65 | return deferred.promise; 66 | } 67 | 68 | /** 69 | * Returns the appropriate base url for an endpoint 70 | * 71 | * @param {Boolean} isProfiles Determines which endpoint type to return; 72 | * @return {string} The URL endpoint for the query without parameters. 73 | */ 74 | function getQueryUrl(isProfiles) { 75 | var profilesEndpoint = getProfileEndpoint(ConfigService.getQueryProfile(), 'select'); 76 | var pipelinesEndpoint = getPipelineEndpoint(ConfigService.getQueryPipeline(), 'select'); 77 | 78 | return isProfiles ? profilesEndpoint : pipelinesEndpoint; 79 | } 80 | 81 | function getProfileEndpoint(profile, requestHandler){ 82 | return ApiBase.getEndpoint() + 'api/apollo/collections/' + 83 | ConfigService.getCollectionName() + '/query-profiles/' + 84 | profile + '/' + requestHandler; 85 | } 86 | 87 | function getPipelineEndpoint(pipeline, requestHandler){ 88 | return ApiBase.getEndpoint() + 'api/apollo/query-pipelines/' + 89 | pipeline + '/collections/' + ConfigService.getCollectionName() + 90 | '/' + requestHandler; 91 | } 92 | 93 | } 94 | } 95 | })(); 96 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /*eslint-env node*/ 2 | // FUSION SEED APP GULPFILE 3 | // ------------------------------------- 4 | // This file processes all of the assets in the "client" folder, combines them with the Foundation for Apps assets, and outputs the finished files in the "build" folder as a finished app. 5 | 6 | // 1. LIBRARIES 7 | // - - - - - - - - - - - - - - - 8 | // 9 | 10 | /* 11 | * gulpfile.js 12 | * =========== 13 | * Rather than manage one giant configuration file responsible 14 | * for creating multiple tasks, each task has been broken out into 15 | * its own file in the 'gulp' folder. Any files in that directory get 16 | * automatically required below. 17 | * 18 | * To add a new task, simply add a new task file in that directory. 19 | */ 20 | 21 | var gulp = require('gulp'); 22 | var argv = require('yargs').argv; 23 | var requireDir = require('require-dir'); 24 | 25 | // 2. FILE PATHS 26 | // - - - - - - - - - - - - - - - 27 | // Specify paths & globbing patterns for tasks. 28 | global.paths = { 29 | assets: [ 30 | './client/**/*.*', 31 | '!./client/templates/**/*.*', 32 | '!./client/assets/{scss,js,components}/**/*.*' 33 | ], 34 | // Sass will check these folders for files when you use @import. 35 | sass: [ 36 | 'client/assets/scss', 37 | 'bower_components/foundation-apps/scss', 38 | 'client/assets/components/**/*.scss' 39 | ], 40 | // These files include Foundation for Apps and its dependencies 41 | foundationJS: [ 42 | 'bower_components/lodash/lodash.js', 43 | 'bower_components/fastclick/lib/fastclick.js', 44 | 'bower_components/viewport-units-buggyfill/viewport-units-buggyfill.js', 45 | 'bower_components/tether/tether.js', 46 | 'bower_components/hammerjs/hammer.js', 47 | 'bower_components/angular/angular.js', 48 | 'bower_components/angular-animate/angular-animate.js', 49 | 'bower_components/angular-sanitize/angular-sanitize.js', 50 | 'bower_components/angular-ui-router/release/angular-ui-router.js', 51 | 'bower_components/angular-mass-autocomplete/massautocomplete.js', 52 | 'bower_components/angular-rison/dist/angular-rison.js', 53 | 'bower_components/foundation-apps/js/vendor/**/*.js', 54 | 'bower_components/foundation-apps/js/angular/**/*.js', 55 | '!bower_components/foundation-apps/js/angular/app.js', 56 | 'bower_components/ng-orwell/Orwell.js', 57 | 'bower_components/humanize/humanize.js', 58 | 'bower_components/angularjs-humanize/src/angular-humanize.js' 59 | ], 60 | // These files are for your app's JavaScript 61 | appJS: [ 62 | 'client/assets/js/app.js', 63 | 'client/assets/js/services/*.js', 64 | 'client/assets/js/controllers/*.js', 65 | 'client/assets/js/utils/**/*.js', 66 | 'client/assets/components/**/*.js' 67 | ], 68 | components: [ 69 | 'client/assets/components/**/*.html', 70 | 'bower_components/foundation-apps/js/angular/components/**/*.html' 71 | ], 72 | configJS: [ 73 | './FUSION_CONFIG.js' 74 | ], 75 | configJSSample: [ 76 | './FUSION_CONFIG.sample.js' 77 | ] 78 | }; 79 | 80 | // Check for --production flag 81 | global.isProduction = !!(argv.production); 82 | 83 | // 3. TASKS 84 | // - - - - - - - - - - - - - - - 85 | 86 | // Require all tasks in the 'gulp' folder. 87 | requireDir('./gulp', { recurse: false }); 88 | 89 | 90 | // Default task 91 | // 92 | // builds your app, starts a server, and recompiles assets when they change. 93 | 94 | gulp.task('default', ['serve']); 95 | -------------------------------------------------------------------------------- /client/assets/js/utils/DocsHelper.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | angular 3 | .module('lucidworksView.utils.docs', []) 4 | .factory('DocsHelper', DocsHelper); 5 | 6 | /** 7 | * DocsHelper 8 | * 9 | * @param {Service} _ lodash 10 | * @return {Object} The properties 11 | */ 12 | function DocsHelper(_) { 13 | 'ngInject'; 14 | return { 15 | populateFieldLabels: populateFieldLabels, 16 | concatMultivaluedFields: concatMultivaluedFields, 17 | parseWildcards: parseWildcards, 18 | selectFields: selectFields 19 | }; 20 | 21 | /** 22 | * Select Fields given a document and fieldArray 23 | * @param {object} document [description] 24 | * @param {array} fieldArray [description] 25 | * @return {object} The selected fields 26 | */ 27 | function selectFields(document, fieldArray) { 28 | return _.pick(document, fieldArray); 29 | } 30 | 31 | /** 32 | * Parse a fields list for a wildcard, if present return an array of all fields. 33 | * @param {array} fieldsList List of fields and/or wildcards. 34 | * @param {object} doc The document fields. 35 | * @return {array} A list of all applicable fields. 36 | */ 37 | function parseWildcards(fieldsList, doc){ 38 | var wildcardId = _.indexOf(fieldsList, '*'); 39 | if(wildcardId > -1){ 40 | // Slice the list to just the fields before the first wildcard to order them first. 41 | var fieldsBeforeWildcard = _.slice(fieldsList, 0, wildcardId); 42 | // Add all existing fields to wildcard. 43 | fieldsList = _.union(fieldsBeforeWildcard, Object.keys(doc)); 44 | // Remove the angular hashKey. 45 | _.remove(fieldsList, function(n) { 46 | switch(n){ 47 | case '$$hashKey': 48 | return true; 49 | } 50 | return false; 51 | }); 52 | } 53 | return fieldsList; 54 | } 55 | 56 | /** 57 | * Returns human readable field names for a document 58 | * 59 | * @param {object} document The document objects to populate. 60 | * @param {object} fieldLabelMap The field to label map. 61 | * @return {object} A version of the document with populated labels. 62 | */ 63 | function populateFieldLabels(document, fieldLabelMap) { 64 | //TODO: populate the field names from the map 65 | 66 | var unzippedDocuments = _.chain(document) 67 | .map(function (value, key) { 68 | return fieldLabelMap.hasOwnProperty(key) ? { 69 | key: fieldLabelMap[key], 70 | value: value 71 | } : { 72 | key: key, 73 | value: value 74 | }; 75 | }) 76 | .value(); 77 | var blankDocument = {}; 78 | _.forEach(unzippedDocuments, function (pair) { 79 | blankDocument[pair.key] = pair.value; 80 | }); 81 | return blankDocument; 82 | } 83 | 84 | /** 85 | * concatMultivaluedFields Concats all multi-value Solr fields 86 | * @param {Object} document The document 87 | * @return {Object} the document with joined mulitvalued fields 88 | */ 89 | function concatMultivaluedFields(document) { 90 | var blankDocument = {}; 91 | _.forEach(document, function (value, key) { 92 | blankDocument[key] = (value instanceof Array) ? value.join(' ') : value; 93 | }); 94 | return blankDocument; 95 | } 96 | } 97 | })(); 98 | -------------------------------------------------------------------------------- /client/assets/components/facetRange/facetRangeService.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular 5 | .module('lucidworksView.components.facetRange') 6 | .factory('FacetRangeService', FacetRangeService); 7 | 8 | function FacetRangeService(){ 9 | return { 10 | getEnd: getEnd, 11 | isDateString: isDateString 12 | }; 13 | } 14 | 15 | function getEnd(start, gapStr){ 16 | var end; 17 | if(isDateString(start)){ 18 | end = getEndDate(start, gapStr); 19 | } else if(!(isNaN(start) || isNaN(gapStr))){ 20 | end = Number(start) + Number(gapStr); 21 | } else { 22 | end = null; 23 | } 24 | 25 | return end; 26 | } 27 | 28 | function isDateString(start){ 29 | var dateStringTestRegex = /\d{4}-\d{2}-\d{2}T\d{2}\:\d{2}\:\d{2}([+-]\d{2}\:\d{2})|Z/; 30 | if(angular.isString(start)){ 31 | return (start.search(dateStringTestRegex) !== -1); 32 | } else { 33 | return false; 34 | } 35 | } 36 | 37 | /** 38 | * solve for End of date range using gap returned from solr 39 | */ 40 | function getEndDate(start, gapStr) { 41 | var regex = /([-+])(\d+)(\w+)(\/\w+)?/g; 42 | var returnDate = new Date(start); 43 | var match = regex.exec(gapStr); 44 | while (match != null) { 45 | var sign = match[1]; 46 | var value = Number(match[2]); 47 | var unit = match[3]; 48 | 49 | /*eslint indent: ["error", 2, { "SwitchCase": 1 }]*/ 50 | switch(unit) { 51 | case 'MILLI': 52 | case 'MILLIS': 53 | case 'MILLISECONDS': 54 | if (sign === '-') { 55 | returnDate.setMilliseconds(returnDate.getMilliseconds() - value); 56 | } else { 57 | returnDate.setMilliseconds(returnDate.getMilliseconds() + value); 58 | } 59 | break; 60 | 61 | case 'SECOND': 62 | case 'SECONDS': 63 | if (sign === '-') { 64 | returnDate.setSeconds(returnDate.getSeconds() - value); 65 | } else { 66 | returnDate.setSeconds(returnDate.getSeconds() + value); 67 | } 68 | break; 69 | 70 | case 'HOUR': 71 | case 'HOURS': 72 | if (sign === '-') { 73 | returnDate.setHours(returnDate.getHours() - value); 74 | } else { 75 | returnDate.setHours(returnDate.getHours() + value); 76 | } 77 | break; 78 | 79 | case 'DATE': 80 | case 'DAY': 81 | case 'DAYS': 82 | if (sign === '-') { 83 | returnDate.setDate(returnDate.getDate() - value); 84 | } else { 85 | returnDate.setDate(returnDate.getDate() + value); 86 | } 87 | break; 88 | 89 | case 'MONTH': 90 | case 'MONTHS': 91 | if (sign === '-') { 92 | returnDate.setMonth(returnDate.getMonth() - value); 93 | } else { 94 | returnDate.setMonth(returnDate.getMonth() + value); 95 | } 96 | break; 97 | 98 | case 'YEAR': 99 | case 'YEARS': 100 | if (sign === '-') { 101 | returnDate.setYear(returnDate.getYear() - value); 102 | } else { 103 | returnDate.setFullYear(returnDate.getFullYear() + value); 104 | } 105 | break; 106 | } 107 | 108 | match = regex.exec(gapStr); 109 | } 110 | return returnDate.toISOString(); 111 | } 112 | })(); 113 | -------------------------------------------------------------------------------- /client/assets/components/paginate/paginateService.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular 5 | .module('lucidworksView.components.paginate') 6 | .factory('PaginateService', PaginateService); 7 | 8 | 9 | function PaginateService(Orwell) { 10 | 'ngInject'; 11 | var queryObservable, resultsObservable, 12 | service = { 13 | pageToStartRow: pageToStartRow, 14 | getRowsPerPage: getRowsPerPage, 15 | getCurrentPage: getCurrentPage, 16 | getTotalPages: getTotalPages, 17 | getNormalizedCurrentPage: getNormalizedCurrentPage 18 | }; 19 | 20 | activate(); 21 | 22 | return service; 23 | 24 | //////////////// 25 | 26 | /** 27 | * Activate the service. 28 | */ 29 | function activate(){ 30 | queryObservable = Orwell.getObservable('query'); 31 | resultsObservable = Orwell.getObservable('queryResults'); 32 | } 33 | 34 | /** 35 | * Turn a page into a start row. 36 | * @param {integer} page The page index 37 | * @return {integer} The start row 38 | */ 39 | function pageToStartRow(page) { 40 | return page * getRowsPerPage(); 41 | } 42 | 43 | /** 44 | * Get the number of Rows Per Page 45 | * 46 | * @return {integer} Rows per page 47 | */ 48 | function getRowsPerPage() { 49 | var query = queryObservable.getContent(); 50 | return query.rows; 51 | } 52 | 53 | /** 54 | * Get the current page 55 | * @return {integer} The page number 56 | */ 57 | function getCurrentPage() { 58 | return getPage(getCurrentStartRow()); 59 | } 60 | 61 | /** 62 | * Get the total number of pages for the query. 63 | * @return {integer} The number of pages 64 | */ 65 | function getTotalPages() { 66 | if (getRowsPerPage() === 0) return 0; 67 | // total pages = CEIL(ALL ROWS / ROWS PER PAGE) 68 | return (getTotalResultRows() > 0) ? (Math.ceil(getTotalResultRows() / 69 | getRowsPerPage())) : 0; 70 | } 71 | 72 | ///////////////////// 73 | // Private Methods // 74 | ///////////////////// 75 | 76 | /** 77 | * Get the current start row. 78 | * @return {integer} The start row 79 | */ 80 | function getCurrentStartRow() { 81 | var query = queryObservable.getContent(); 82 | return query.start; 83 | } 84 | 85 | /** 86 | * Get the total number of results rows for the current query. 87 | * @return {integer} Number of result rows. 88 | */ 89 | function getTotalResultRows() { 90 | var results = resultsObservable.getContent(); 91 | if (results.hasOwnProperty('response')) { 92 | return results.response.numFound; 93 | } 94 | return 0; 95 | } 96 | 97 | /** 98 | * Get a page given a start row. 99 | * @param {integer} startRow The row to start the page from 100 | * @return {integer} The page number 101 | */ 102 | function getPage(startRow){ 103 | if (getRowsPerPage() === 0 || startRow === 0) return 0; 104 | return Math.ceil(startRow / getRowsPerPage()); 105 | } 106 | 107 | /** 108 | * Get the current page and normalize it wrt 1 109 | * @return {integer} [Normalized current page value] 110 | */ 111 | function getNormalizedCurrentPage(){ 112 | return getCurrentPage() + 1; 113 | } 114 | 115 | 116 | } 117 | })(); 118 | -------------------------------------------------------------------------------- /client/assets/components/paginate/paginateDirective.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular 5 | .module('lucidworksView.components.paginate') 6 | .directive('paginate', paginate); 7 | 8 | function paginate() { 9 | 'ngInject'; 10 | var directive = { 11 | restrict: 'EA', 12 | templateUrl: 'assets/components/paginate/paginate.html', 13 | controller: Controller, 14 | controllerAs: 'vm', 15 | bindToController: true, 16 | scope: true 17 | }; 18 | 19 | return directive; 20 | 21 | } 22 | 23 | function Controller(Orwell, PaginateService, QueryService, $filter) { 24 | 'ngInject'; 25 | var vm = this; 26 | vm.page = 0; 27 | vm.totalPages = 0; 28 | vm.getNormalizedPage = getNormalizedPage; 29 | vm.getNormalizedPageFormatted = getNormalizedPageFormatted; 30 | vm.getLastPage = getLastPage; 31 | vm.gotoNextPage = gotoNextPage; 32 | vm.gotoPreviousPage = gotoPreviousPage; 33 | vm.showState = 'next'; 34 | 35 | activate(); 36 | 37 | ///////////// 38 | 39 | function activate() { 40 | var resultsObservable = Orwell.getObservable('queryResults'); 41 | 42 | resultsObservable.addObserver(function (data) { 43 | if (data.hasOwnProperty('response')) { 44 | vm.page = PaginateService.getCurrentPage(); 45 | vm.totalPages = PaginateService.getTotalPages(); 46 | vm.totalPagesFormatted = $filter('humanizeNumberFormat')(vm.totalPages, 0); 47 | vm.showState = pickPaginatorType(); 48 | } else { 49 | vm.page = 0; 50 | vm.totalPages = 0; 51 | } 52 | }); 53 | } 54 | 55 | function pickPaginatorType(){ 56 | if(vm.totalPages < 1) return 'neither'; 57 | if(vm.page === 0 && vm.totalPages > 1) return 'next'; 58 | if(vm.page === vm.getLastPage() && vm.page > 0) return 'previous'; 59 | if(vm.page !== vm.getLastPage()) return 'both'; 60 | } 61 | 62 | /** 63 | * Get the page number (normalized for viewers from a 1 base) 64 | * @return {integer} normalized page number 65 | */ 66 | function getNormalizedPage() { 67 | return vm.page + 1; 68 | } 69 | 70 | function getNormalizedPageFormatted(){ 71 | return $filter('humanizeNumberFormat')(getNormalizedPage(), 0); 72 | } 73 | 74 | /** 75 | * Get the last page (0 start based) 76 | * @return {integer} The last page from 0 base. 77 | */ 78 | function getLastPage() { 79 | return vm.totalPages - 1; 80 | } 81 | 82 | /** 83 | * Updates the query parameters to the next page. 84 | */ 85 | function gotoNextPage() { 86 | gotoPage(PaginateService.getCurrentPage() + 1); 87 | } 88 | 89 | /** 90 | * Updates the query parameters to the previous page. 91 | */ 92 | function gotoPreviousPage() { 93 | gotoPage(PaginateService.getCurrentPage() - 1); 94 | } 95 | 96 | /** 97 | * Actually updates the page to go to. 98 | * @param {integer} page The page to do to 99 | */ 100 | function gotoPage(page) { 101 | if (page < 0) return; 102 | if (page > PaginateService.getTotalPages()) return; 103 | if (page === PaginateService.getCurrentPage()) return; 104 | // This will change the query and cause the interface to make an http call. 105 | QueryService.setQuery({ 106 | start: PaginateService.pageToStartRow(page) 107 | }); 108 | } 109 | 110 | } 111 | })(); 112 | -------------------------------------------------------------------------------- /client/templates/home.html: -------------------------------------------------------------------------------- 1 | --- 2 | name: home 3 | url: /search?query 4 | controller: HomeController as hc 5 | --- 6 | 7 |
8 | 9 | 10 | 47 | 60 | 61 |
62 | 63 |
64 |
{{hc.numFoundFormatted}} results
65 |
66 |
67 | 68 | 69 | 70 | 71 | 72 | 73 | Sort 74 | 75 |
    76 |
  • 77 | 78 |
  • 79 |
80 |
81 |
82 |
83 | 84 | 85 | 86 |
87 | 88 |
89 | -------------------------------------------------------------------------------- /client/assets/components/document/document_default/document_default.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | angular.module('lucidworksView.components.document', ['lucidworksView.services.config', 3 | 'lucidworksView.utils.docs', 'lucidworksView.services.signals' 4 | ]) 5 | .directive('documentDefault', documentDefault); 6 | 7 | 8 | function documentDefault() { 9 | 'ngInject'; 10 | return { 11 | templateUrl: 'assets/components/document/document_default/document_default.html', 12 | scope: true, 13 | controller: Controller, 14 | controllerAs: 'vm', 15 | bindToController: { 16 | doc: '=', 17 | position: '=', 18 | highlight: '=' 19 | }, 20 | replace: true 21 | }; 22 | } 23 | 24 | function Controller($log, $scope, DocsHelper, ConfigService, SignalsService, PaginateService, DocumentService) { 25 | 'ngInject'; 26 | var vm = this; 27 | vm.postSignal = postSignal; 28 | var templateFields = []; 29 | var specialFields = ['head', 'subhead', 'description', 'image', 'head_url']; 30 | 31 | activate(); 32 | 33 | /////////// 34 | 35 | function activate() { 36 | vm.doc = processDocument(DocsHelper.concatMultivaluedFields(vm.doc)); 37 | _.forEach(specialFields, function(fieldType) { 38 | templateFields.push(ConfigService.getFields.get(fieldType)); 39 | }); 40 | } 41 | 42 | /** 43 | * Processes a document prepares fields from the config for display. 44 | * @param {object} doc A single document record 45 | * @return {object} The document record with processed properties. 46 | */ 47 | function processDocument(doc) { 48 | 49 | // Populate the additional fields to display 50 | 51 | // Get fields from config service. 52 | var fieldsToDisplay = ConfigService.getFieldsToDisplay(); 53 | // Parse any wildcards in the config. 54 | fieldsToDisplay = DocsHelper.parseWildcards(fieldsToDisplay, doc); 55 | // turn fields to display into a list of params. 56 | doc.fieldsToDisplay = DocsHelper.populateFieldLabels( 57 | DocsHelper.selectFields(doc, fieldsToDisplay), 58 | ConfigService.getFieldLabels() 59 | ); 60 | 61 | doc.lw_head = { 62 | key: getTemplateDisplayFieldName(ConfigService.getFields.get('head')), 63 | value: getField('head', doc) ? getField('head', doc) : 'Title Field Not Found' 64 | }; 65 | 66 | doc.lw_subhead = { 67 | key: getTemplateDisplayFieldName(ConfigService.getFields.get('subhead')), 68 | value: getField('subhead', doc) 69 | }; 70 | 71 | doc.lw_description = { 72 | key: getTemplateDisplayFieldName(ConfigService.getFields.get('description')), 73 | value: getField('description', doc) 74 | }; 75 | 76 | doc.lw_image = { 77 | key: getTemplateDisplayFieldName(ConfigService.getFields.get('image')), 78 | value: getField('image', doc) ? DocumentService.decodeFieldValue(doc, ConfigService.getFields.get('image')) : null 79 | }; 80 | 81 | doc.lw_url = { 82 | key: getTemplateDisplayFieldName(ConfigService.getFields.get('head_url')), 83 | value: getField('head_url', doc) ? DocumentService.decodeFieldValue(doc, ConfigService.getFields.get('head_url')) : null 84 | }; 85 | 86 | doc._signals = DocumentService.setSignalsProperties(doc, vm.position); 87 | 88 | return doc; 89 | } 90 | 91 | /** 92 | * Given a field type get the actual field value. 93 | * @param {String} fieldType The Field type. 94 | * @param {object} doc The document object 95 | * @return {String|null} The field value. 96 | */ 97 | function getField(fieldType, doc) { 98 | var fieldName = ConfigService.getFields.get(fieldType); 99 | if (doc.hasOwnProperty(fieldName)) { 100 | return doc[fieldName]; 101 | } 102 | return null; 103 | } 104 | 105 | function postSignal(options){ 106 | DocumentService.postSignal(vm.doc._signals, options); 107 | } 108 | 109 | function getTemplateDisplayFieldName(field){ 110 | return DocumentService.getTemplateDisplayFieldName(vm.doc, field); 111 | } 112 | } 113 | })(); 114 | -------------------------------------------------------------------------------- /client/assets/components/searchBox/searchBoxDirective.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular 5 | .module('lucidworksView.components.searchbox') 6 | .directive('searchBox', searchbox); 7 | 8 | function searchbox() { 9 | 'ngInject'; 10 | return { 11 | restrict: 'EA', 12 | controller: Controller, 13 | templateUrl: 'assets/components/searchBox/searchBox.html', 14 | scope: true, 15 | controllerAs: 'ta', 16 | bindToController: { 17 | onSelect:'&', 18 | query: '=' 19 | }, 20 | require: '^form' 21 | }; 22 | } 23 | 24 | function Controller($element, $log, $q, $sce, $timeout, ConfigService, SearchBoxDataService) { 25 | 'ngInject'; 26 | var ta = this; 27 | 28 | ta.checkKeyPress = checkKeyPress; 29 | ta.typeaheadField = ConfigService.getTypeaheadField(); 30 | ta.initialValue = _.isArray(ta.query)?ta.query[0]:ta.query; 31 | ta.noResults = undefined; 32 | 33 | //need to get hold of the element to be able to manually close the suggestions div 34 | var massAutocompleteElem = $element.find('div')[0]; 35 | 36 | //mass-autocomplete config 37 | ta.dirty = {}; 38 | 39 | ta.autocomplete_options = { 40 | suggest: doTypeaheadSearch, 41 | on_error: showNoResults, 42 | on_select: selectedSomething, 43 | }; 44 | 45 | activate(); 46 | 47 | function activate() { 48 | ta.dirty.value = ta.query; 49 | } 50 | 51 | function checkKeyPress($event) { 52 | ta.query = ta.dirty.value; 53 | if ($event.keyCode === 13) { 54 | closeSuggester(); 55 | } 56 | } 57 | 58 | function closeSuggester() { 59 | var massAutoElemScope = angular.element(massAutocompleteElem).isolateScope(); 60 | $timeout(function() { 61 | if(massAutoElemScope.show_autocomplete) { 62 | massAutoElemScope.show_autocomplete = false; 63 | } 64 | },200); 65 | } 66 | 67 | function doTypeaheadSearch(term) { 68 | var deferred = $q.defer(); 69 | ta.noResults = false; 70 | // set this here 71 | setQuery(term); 72 | SearchBoxDataService 73 | .getTypeaheadResults({q: term, wt: 'json'}) 74 | .then(function (resp) { 75 | if(resp.hasOwnProperty('response') && resp.response.docs.length) { 76 | deferred.resolve(suggest_results(resp.response.docs,term)); 77 | } else { 78 | return deferred.reject('No suggestions for '+term); 79 | } 80 | }) 81 | .catch(function (error) { 82 | //TODO better error reporting 83 | $log.error('typeahead search error:',error); 84 | // currently don't want to surface these in the UI 85 | // return deferred.reject('An error occurred: '+error); 86 | }); 87 | 88 | return deferred.promise; 89 | } 90 | 91 | 92 | function highlight(str, term) { 93 | var highlight_regex = new RegExp('('+_.escapeRegExp(term)+')', 'gi'); 94 | return str.replace(highlight_regex, '$1'); 95 | } 96 | 97 | function selectedSomething(object) { 98 | if (object) { 99 | var newValue = _.isArray(object.value) ? object.value[0]:object.value; 100 | setQuery(newValue); 101 | $timeout(ta.onSelect); 102 | } 103 | } 104 | 105 | function setQuery (query) { 106 | ta.query = query; 107 | } 108 | 109 | function showNoResults(message) { 110 | ta.noResults = true; 111 | ta.noResultsMessage = message; 112 | } 113 | 114 | function suggest_results(responseDocs,term) { 115 | if (term.length >= 2) { 116 | var results = []; 117 | _.forEach(responseDocs,function(doc) { 118 | var typeaheadValue = _.isArray(doc[ta.typeaheadField]) ? doc[ta.typeaheadField][0] : doc[ta.typeaheadField]; 119 | 120 | if (typeaheadValue) { 121 | results.push({label:$sce.trustAsHtml('
'+highlight(typeaheadValue, term)+'
'),value:typeaheadValue}); 122 | } 123 | }); 124 | return results; 125 | } 126 | } 127 | 128 | } 129 | })(); 130 | -------------------------------------------------------------------------------- /client/assets/components/documentList/documentList.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 |

{{key}}

15 |
16 |

{{group.groupValue}}

17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 |
30 | 31 |
32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 |
41 |
42 |
43 | -------------------------------------------------------------------------------- /client/assets/components/facetList/facetList.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular 5 | .module('lucidworksView.components.facetList', ['lucidworksView.services.config']) 6 | .directive('facetList', facetList); 7 | 8 | function facetList() { 9 | 'ngInject'; 10 | var directive = { 11 | restrict: 'EA', 12 | templateUrl: 'assets/components/facetList/facetList.html', 13 | scope: true, 14 | controller: Controller, 15 | controllerAs: 'vm', 16 | bindToController: { 17 | isLoading: '=' 18 | } 19 | }; 20 | 21 | return directive; 22 | 23 | } 24 | 25 | function Controller(ConfigService, Orwell, LocalParamsService, $filter) { 26 | 'ngInject'; 27 | var vm = this; 28 | var resultsObservable = Orwell.getObservable('queryResults'); 29 | vm.facets = []; 30 | vm.facetNames = {}; 31 | vm.facetLocalParams = {}; 32 | vm.defaultRangeFacetFormatter = defaultRangeFacetFormatter; 33 | activate(); 34 | 35 | /** 36 | * Try to parse the range facets automatically by looking to see if they are dates. Callers 37 | * of the directive may provider their own formatter by specifying the `formatting-handler` attribute 38 | * on the directive. 39 | * @param toFormat 40 | * @returns The formatted result. If it is a date, it will apply the 'mediumDate' format, else it will return the value as is 41 | */ 42 | function defaultRangeFacetFormatter(toFormat){ 43 | //check to see if it looks like a date first, since Solr will likely send back as an ISO date 44 | //2016-02-14T00:00:00Z - 2016-03-14T00:00:00Z 45 | var result; 46 | if (angular.isFunction(toFormat.indexOf) && toFormat.indexOf('Z') != -1 && toFormat.indexOf('T') != -1){ 47 | result = Date.parse(toFormat); 48 | //most range facet displays don't need time info, so just do angular filter by default to day/month/year 49 | result = $filter('date')(result, 'mediumDate'); 50 | } else { 51 | result = toFormat; 52 | } 53 | return result; 54 | } 55 | 56 | function activate() { 57 | resultsObservable.addObserver(function (data) { 58 | // Exit early if there are no facets in the response. 59 | if (!data.hasOwnProperty('facet_counts')) return; 60 | vm.facetLocalParams = LocalParamsService.getLocalParams(data.responseHeader.params); 61 | 62 | // Iterate through each facet type. 63 | _.forEach(data.facet_counts, resultFacetParse); 64 | 65 | function resultFacetParse(resultFacets, facetType){ 66 | // Keep a list of facet names and only reflow facets based on changes to this list. 67 | var facetFields = Object.keys(resultFacets); 68 | if (!_.isEqual(vm.facetNames[facetType], facetFields)) { 69 | var oldFields = _.difference(vm.facetNames[facetType], facetFields); 70 | var newFields = _.difference(facetFields, vm.facetNames[facetType]); 71 | 72 | // Creating temp facet so that we don't have to change the model in Angular 73 | var tempFacets = _.clone(vm.facets); 74 | 75 | //removing old fields 76 | _.forEach(oldFields, function(field){ 77 | _.remove(tempFacets, function(item){ 78 | return item.name === field && item.type === facetType; 79 | }); 80 | }); 81 | 82 | // Adding new facet entries 83 | var newFacets = []; 84 | _.forEach(newFields, function(value){ 85 | var facet = { 86 | name: value, 87 | type: facetType, 88 | autoOpen: true, 89 | label: ConfigService.getFieldLabels()[value]||value, 90 | tag: LocalParamsService.getLocalParamTag(vm.facetLocalParams[retrieveFacetType(facetType)], value) || null 91 | }; 92 | newFacets.push(facet); 93 | }); 94 | 95 | // Updating the list till the end. 96 | tempFacets = _.concat(tempFacets, newFacets); 97 | vm.facets = tempFacets; 98 | 99 | // Updating the reflow deciding list. 100 | vm.facetNames[facetType] = facetFields; 101 | } 102 | } 103 | }); 104 | } 105 | 106 | /** 107 | * Retrieves the facet type from the facetType variable 108 | * @param {string} facetType facet type present in responseHeader.params 109 | * @return {string} facet type split from the initial string 110 | */ 111 | function retrieveFacetType(facetType){ 112 | //example: @param: facet_fields, @return: field 113 | return facetType.split('_')[1].slice(0,-1); 114 | } 115 | 116 | } 117 | })(); 118 | -------------------------------------------------------------------------------- /client/assets/components/documentList/documentList.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular 5 | .module('lucidworksView.components.documentList', ['lucidworksView.services.config', 6 | 'ngOrwell', 'lucidworksView.services.landingPage' 7 | ]) 8 | .directive('documentList', documentList); 9 | 10 | function documentList() { 11 | 'ngInject'; 12 | return { 13 | restrict: 'EA', 14 | templateUrl: 'assets/components/documentList/documentList.html', 15 | controller: Controller, 16 | controllerAs: 'vm', 17 | bindToController: {}, 18 | scope: true, 19 | replace: true 20 | }; 21 | 22 | } 23 | 24 | function Controller($sce, $log, $anchorScroll, Orwell) { 25 | 'ngInject'; 26 | var vm = this; 27 | vm.docs = []; 28 | vm.highlighting = {}; 29 | vm.getDocType = getDocType; 30 | vm.groupedResults = false; 31 | vm.toggleGroupedResults = toggleGroupedResults; 32 | vm.showGroupedResults = {}; 33 | vm.getDocPosition = getDocPosition; 34 | 35 | activate(); 36 | 37 | //////// 38 | 39 | function activate() { 40 | var resultsObservable = Orwell.getObservable('queryResults'); 41 | resultsObservable.addObserver(function (data) { 42 | vm.docs = parseDocuments(data); 43 | vm.highlighting = parseHighlighting(data); 44 | vm.getDoctype = getDocType; 45 | $anchorScroll('topOfMainContent'); 46 | }); 47 | } 48 | 49 | /** 50 | * Get the document type for the document. 51 | * @param {object} doc Document object 52 | * @return {string} Type of document 53 | */ 54 | function getDocType(doc){ 55 | // Change to your collection datasource type name 56 | // if(doc['_lw_data_source_s'] === 'MyDatasource-default'){ 57 | // return doc['_lw_data_source_s']; 58 | // } 59 | return doc['_lw_data_source_type_s']; 60 | } 61 | 62 | /** 63 | * Decorates the document object before sending to the document directive. 64 | * @param {object} doc Document object 65 | * @return {object} Document object 66 | */ 67 | 68 | function isNotGrouped(data){ 69 | return _.has(data, 'response'); 70 | } 71 | function isGrouped(data){ 72 | return _.has(data, 'grouped'); 73 | } 74 | /** 75 | * Get the documents from 76 | * @param {object} data The result data. 77 | * @return {array} The documents returned 78 | */ 79 | function parseDocuments(data){ 80 | var docs = []; 81 | if (isNotGrouped(data)) { 82 | docs = data.response.docs; 83 | } 84 | else if(isGrouped(data)){ 85 | vm.groupedResults = data.grouped; 86 | parseGrouping(vm.groupedResults); 87 | } 88 | return docs; 89 | } 90 | 91 | 92 | function toggleGroupedResults(toggle){ 93 | vm.showGroupedResults[toggle] = !vm.showGroupedResults[toggle]; 94 | } 95 | 96 | function parseGrouping(results){ 97 | _.each(results, function(item){ 98 | if (_.has(item, 'groups')){ 99 | _.each(item.groups, function(group){ 100 | if(_.has(group, 'groupValue') && group.groupValue !== null){ 101 | vm.showGroupedResults[group.groupValue] = false; 102 | } 103 | else{ 104 | vm.showGroupedResults['noGroupedValue'] = true; 105 | } 106 | }); 107 | } 108 | else{ 109 | vm.groupedDocs = item.doclist.docs; 110 | vm.showGroupedResults['simpleGrouped'] = true; 111 | } 112 | }); 113 | } 114 | 115 | /** 116 | * Get highlighting from a document. 117 | * @param {object} data The result data. 118 | * @return {object} The highlighting results. 119 | */ 120 | function parseHighlighting(data) { 121 | if (data.hasOwnProperty('highlighting')){ 122 | _.each(data.highlighting, function(value, key){ 123 | var vals = {}; 124 | if (value) { 125 | _.each(Object.keys(value), function (key) { 126 | var val = value[key]; 127 | _.each(val, function(high){ 128 | vals[key] = $sce.trustAsHtml(high); 129 | }); 130 | }); 131 | vm.highlighting[key] = vals; 132 | } 133 | }); 134 | } 135 | else{ 136 | vm.highlighting = {}; 137 | } 138 | return vm.highlighting; 139 | } 140 | 141 | /** 142 | * Get index of the doc in the returned documentList 143 | * @param {object} doc Doc of which the index is required 144 | * @param {object} docs List of returned documents 145 | * @return {number} The index of the document in the documentList 146 | */ 147 | function getDocPosition(doc, docs){ 148 | return _.findIndex(docs, doc); 149 | } 150 | } 151 | })(); 152 | -------------------------------------------------------------------------------- /client/assets/js/services/AuthInterceptor.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular 5 | .module('lucidworksView.services.authInterceptor', ['lucidworksView.services.config']) 6 | .factory('AuthInterceptor', AuthInterceptor); 7 | 8 | 9 | function AuthInterceptor(URLService, $q, $log, $rootScope, $injector) { 10 | 'ngInject'; 11 | var tryingAnon = false; 12 | return { 13 | responseError: responseError 14 | }; 15 | 16 | function responseError(resp) { 17 | var deferred = $q.defer(); 18 | var $state = $injector.get('$state'); 19 | // CASE: If the app is on login page, the code is 401 and the request wasn't a api/session POST, then only attempt anon login 20 | // The reason for this check so that not all the response errors are intercepted 21 | if (!$state.is('login') && (resp.status === 401) && !isLoginRequest(resp.config)) { 22 | //CASE: If not already trying anonymous session then do try... 23 | if(!tryingAnon) { 24 | //CASE: If there are usable anon creds, then try it out 25 | if(useAnonCreds()){ 26 | $log.info('Creating anonymous session with credentials from FUSION_CONFIG.js'); 27 | getAnonSession().then(function(){ 28 | //CASE: If anonymous session creation is successful, go home 29 | $log.info('Created anonymous session'); 30 | deferred.reject(); 31 | $state.go('home', prepareQueryForRedirect()); 32 | },function(err){ 33 | // TODO: Investigate why 201 is going to error handler... 34 | // If it's the expected behaviour, figure out a better solution 35 | //CASE: If anonymous login succeeded with a 201 response, go to `home` 36 | if(err.status === 201){ 37 | $log.info('Created anonymous session'); 38 | $state.go('home', prepareQueryForRedirect()); 39 | } 40 | //CASE: If anonymous login failed, then go to login 41 | else{ 42 | $log.info('Failed to create anonymous session'); 43 | $state.go('login', prepareQueryForRedirect()); 44 | } 45 | deferred.reject(err); 46 | }); 47 | } 48 | else{ 49 | //CASE: If anonymous login creds are unusable then go to login 50 | deferred.reject(); 51 | $state.go('login', prepareQueryForRedirect()); 52 | } 53 | } 54 | //CASE: If trying anon login, then that promise chain will take care of stuff 55 | else{ 56 | deferred.reject(); 57 | } 58 | } 59 | //CASE: If unauthorized, don't bother. 60 | else if(resp.status === 403){ 61 | // TODO handle unauthorized users. 62 | $log.warn('You are unauthorized to access that endpoint'); 63 | deferred.reject(false); 64 | } 65 | else{ 66 | deferred.reject(resp); 67 | } 68 | // In all cases reject the promise chain 69 | 70 | function prepareQueryForRedirect() { 71 | var queryObject = URLService.getQueryFromUrl(); 72 | return URLService.convertQueryToStateObject(queryObject); 73 | } 74 | 75 | return deferred.promise; 76 | } 77 | 78 | ////////////// 79 | /// 80 | 81 | /** 82 | * [getAnonSession Creates anonymous session with the given config username:passwoed] 83 | * @return {promise} [Promise for creating the session] 84 | */ 85 | function getAnonSession(){ 86 | var ConfigService = $injector.get('ConfigService'), 87 | AuthService = $injector.get('AuthService'), 88 | $q = $injector.get('$q'); 89 | 90 | tryingAnon = true; 91 | var def = $q.defer(); 92 | AuthService.createSession(ConfigService.config.anonymous_access.username, ConfigService.config.anonymous_access.password) 93 | .then(function(resp){ 94 | tryingAnon = false; 95 | def.reject(resp); 96 | }).catch(function(error){ 97 | tryingAnon = false; 98 | def.reject(error); 99 | }); 100 | return def.promise; 101 | } 102 | 103 | /** 104 | * [useAnonCreds Checks if it's okay to use anonymous credentials] 105 | * @return {Boolean} [Whether it's okay or not] 106 | */ 107 | function useAnonCreds(){ 108 | var ConfigService = $injector.get('ConfigService'), 109 | anonAccess = ConfigService.config.anonymous_access; 110 | 111 | return !(anonAccess.username === '' || anonAccess.password === ''); 112 | } 113 | 114 | /** 115 | * [isLoginRequest Checks if a given resp.config originated from a /api/session POST] 116 | * @param {Object} respConfig [The resp.config object] 117 | * @return {Boolean} [Whether login request or not] 118 | */ 119 | function isLoginRequest(respConfig){ 120 | //TODO: Make this better 121 | var ApiBase = $injector.get('ApiBase'); 122 | return (respConfig.url.indexOf(ApiBase.getEndpoint() + 'api/session') !== -1) && (respConfig.method === 'POST'); 123 | } 124 | 125 | } 126 | })(); 127 | -------------------------------------------------------------------------------- /client/assets/js/services/URLService.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | var blankQuery = { 5 | q: '*', 6 | start: 0, 7 | wt: 'json' 8 | }; 9 | 10 | angular 11 | .module('lucidworksView.services.url', ['rison']) 12 | .constant('BLANK_QUERY', blankQuery) 13 | .constant('QUERY_PARAM', 'query') 14 | .factory('URLService', URLService); 15 | 16 | function URLService(ConfigService, $log, $rison, $injector, $location, 17 | QUERY_PARAM) { 18 | 'ngInject'; 19 | return { 20 | setQueryToURLAndGo: setQueryToURLAndGo, 21 | convertQueryToStateObject: convertQueryToStateObject, 22 | getQueryFromUrl: getQueryFromUrl, 23 | 24 | /// deprecated 25 | setQuery:setQuery 26 | }; 27 | 28 | ////////// 29 | 30 | /** 31 | * setQuery 32 | * DEPRECATED in 1.4 use QueryService.setQuery() instead 33 | **/ 34 | function setQuery(query) { 35 | $log.error('The function URLService.setQuery() was deprecated in Lucidworks View 1.4 use QueryService.setQuery() instead. Will be removed in version 1.5 release.'); 36 | var QueryService = $injector.get('QueryService'); 37 | QueryService.setQuery(query); 38 | } 39 | 40 | /** 41 | * Sets the URL bar 42 | * @param {object} queryObject The query object 43 | */ 44 | function convertQueryToStateObject(queryObject) { 45 | var queryObjectToBeStringed = _.clone(queryObject,true); 46 | //Only need the slashes to get encoded, so that app state doesn't change 47 | queryObjectToBeStringed = encodeSlashes(queryObjectToBeStringed); 48 | var queryObjectString = $rison.stringify(queryObjectToBeStringed); 49 | var newStateObject = {}; 50 | newStateObject[QUERY_PARAM] = queryObjectString; 51 | return newStateObject; 52 | } 53 | 54 | function setQueryToURLAndGo(queryObject) { 55 | var newStateObject = convertQueryToStateObject(queryObject); 56 | var $state = $injector.get('$state'); 57 | // Adding reloadOnSearch:false for now fixes the double reload bug SU-60 58 | // @see http://stackoverflow.com/a/22863315 59 | $state.go('home', newStateObject, {notify: false, reloadOnSearch: false}); 60 | } 61 | 62 | /** 63 | * Gets query object from URL 64 | */ 65 | function getQueryFromUrl() { 66 | var queryString = $location.search()[QUERY_PARAM]; 67 | var queryObject; 68 | try{ 69 | queryObject = queryString ? $rison.parse(decodeURIComponent(queryString)):ConfigService.config.default_query; 70 | } 71 | catch(e){ 72 | $log.error('Cannot parse query URL'); 73 | queryObject = ConfigService.config.default_query; 74 | } 75 | var temp = convertTreeArrays(queryObject); 76 | return temp; 77 | } 78 | 79 | ////////////////////////// 80 | /// Internal functions /// 81 | ////////////////////////// 82 | 83 | /** 84 | * Traverses the whole object tree and encodes slashes 85 | */ 86 | function encodeSlashes(queryObject){ 87 | var newQueryObject = {}; 88 | _.forIn(queryObject, function(item, key){ 89 | if(_.isArray(item) || _.isObject(item)){ 90 | newQueryObject[key] = encodeSlashes(item); 91 | } 92 | else if(_.isString(item)){ 93 | newQueryObject[key] = item.replace(/\//g,'%2F'); 94 | } 95 | else{ 96 | newQueryObject[key] = item; 97 | } 98 | }); 99 | return newQueryObject; 100 | } 101 | 102 | /** 103 | * Checks if the `item` is a possible array. 104 | */ 105 | function isArrayable(item){ 106 | if(item instanceof Object && !(item instanceof String)){ 107 | var stuff = _.keys(item); 108 | var stuffToCheck = _.chain(stuff).map(function(value){ 109 | return _.parseInt(value); 110 | }) 111 | .filter(function(value){ 112 | return !_.isNaN(value); 113 | }) 114 | .value(); 115 | return stuff.length === stuffToCheck.length; 116 | } 117 | else{ 118 | return false; 119 | } 120 | } 121 | 122 | /** 123 | * Converts an object to an array. 124 | */ 125 | function objectToArray(item){ 126 | var newArray = []; 127 | var newArrayLength = 0; 128 | _.forIn(item, function(item, key){ 129 | newArrayLength++; 130 | newArray[key] = item; 131 | }); 132 | newArray.length = newArrayLength; 133 | return newArray; 134 | } 135 | 136 | /** 137 | * Traverses the whole object tree, if there is a possible array converts 138 | * that to an array. 139 | * Checking if the object could be an array by checking all the keys are integers. 140 | * Rison specs are inadequate to decide, hence this piece of function 141 | */ 142 | function convertTreeArrays(queryObject){ 143 | var newQueryObject = {}; 144 | 145 | var arrayer = function(item){ 146 | if(isArrayable(item)){ 147 | return objectToArray(item); 148 | } 149 | else{ 150 | return item; 151 | } 152 | }; 153 | 154 | _.forEach(queryObject, function(item, key){ 155 | if(_.isObject(item)){ 156 | newQueryObject[key] = arrayer(convertTreeArrays(item)); 157 | } 158 | else{ 159 | newQueryObject[key] = item; 160 | } 161 | }); 162 | 163 | return newQueryObject; 164 | } 165 | } 166 | })(); 167 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lucidworks View 2 | [Lucidworks View](https://lucidworks.com/products/view/) is a consumer-facing front end for Lucidworks Fusion. It provides a basic search interface with simple configuration, so you can quickly deliver a Fusion-based search solution with minimal development. View is powered by Fusion, Gulp, Angular, and libSaSS. 3 | 4 | You can also use View as the basis for developing a more sophisticated Web interface, using Foundation for Apps: http://foundation.zurb.com/apps/docs/ 5 | 6 | If you need help setting up Fusion, see https://doc.lucidworks.com/. To ask questions about View, see the [Lucidworks View Q&A](https://support.lucidworks.com/hc/en-us/community/topics/200922728-Lucidworks-View-Q-A) site. 7 | 8 | ## Requirements 9 | 10 | If you downloaded a [platform-specific package](https://github.com/lucidworks/lucidworks-view/releases), all dependencies are included. Skip to Get Started step 4. 11 | 12 | If you start by cloning the repository, you'll need the following software: 13 | 14 | - [Node.js](http://nodejs.org): Use the installer for your OS. Use version 5.xxx. 15 | - [Git](http://git-scm.com/downloads) (if you're cloning the repo): Use the installer for your OS. 16 | - Windows users can also try [Git for Windows](http://git-for-windows.github.io/). 17 | - [Gulp](http://gulpjs.com/) and [Bower](http://bower.io): Run `npm install -g gulp bower` 18 | - Depending on how Node is configured on your machine, you may need to run `sudo npm install -g gulp bower` instead, if you get an error with the first command. 19 | 20 | ## Get Started 21 | 22 | 1. Clone the repository, where `app` is the name of your app: 23 | 24 | ```bash 25 | git clone https://github.com/lucidworks/lucidworks-view app 26 | ``` 27 | 28 | 1. Change into the directory: 29 | 30 | ```bash 31 | cd app 32 | ``` 33 | 34 | 1. Install the dependencies: 35 | 36 | ```bash 37 | npm install 38 | bower install 39 | ``` 40 | 41 | 1. While you're working on your project, run: 42 | 43 | * If you downloaded a tar package: 44 | 45 | ```bash 46 | ./view.sh start 47 | ``` 48 | 49 | * If you cloned the repository: 50 | 51 | ```bash 52 | npm start 53 | ``` 54 | 55 | This will compile the SaSS, assemble your Angular app, and create `FUSION_CONFIG.js` (if you haven't created it already). You'll see output that tells you which port was selected: 56 | 57 | ``` 58 | [BS] Access URLs: 59 | ------------------------------------ 60 | Local: http://localhost:3000 61 | External: http://:3000 62 | ------------------------------------ 63 | ``` 64 | 65 | The default is port 3000, but if that port is already in use then the app selects the next highest available port. 66 | 67 | 1. **Now go to `http://localhost:` in your browser to see it in action.** 68 | 69 | The first time you browse to the app, you'll see a login page. Use your Fusion username and password. To enable anonymous access, edit the `anonymous_access` keys in FUSION_CONFIG.js. 70 | 71 | When you change FUSION_CONFIG.js or any file in the `client` folder, the appropriate Gulp task will run to build new files. This uses [`browser-sync`](https://www.browsersync.io/) for instant reload upon change of source files. Visit `http://localhost:3001` (or whatever your terminal shows as the browser-sync UI) for the `browser-sync` dashboard. 72 | 73 | To run the compiling process once, without watching any files, use the `build` command: 74 | ```bash 75 | npm run build 76 | ``` 77 | this command creates a built version of View which can be copied from the build folder to another folder/machine and served on your own webserver. 78 | 79 | For development purposes, you can develop without a minified build by using the command 80 | ```bash 81 | npm run start-dev 82 | ``` 83 | 84 | this command runs a node server, with minimized packages, and works similarly to the `npm start` command. 85 | 86 | ## Unit testing 87 | 88 | ``` 89 | npm run build 90 | npm test 91 | ``` 92 | 93 | ## Basic Configuration 94 | 95 | The first time you run `npm start`, FUSION_CONFIG.sample.js is copied to FUSION_CONFIG.js. Modify this file to configure View's basic options. Documentation about the configuration keys is included in the file. 96 | 97 | At a minimum, you _must_ configure the `collection` key to match the name of your Fusion collection. 98 | 99 | In a production environment, you must also configure `host` and `port` to point to the UI service of your Fusion deployment. The default is `localhost:8764` for development purposes. 100 | 101 | When the app is running with BrowserSync, it reloads the configuration every time you save FUSION_CONFIG.js. You can modify the configuration and watch the app change in real time in your browser. 102 | 103 | ## Basic Customization 104 | 105 | The title and logo for your interface are configured in FUSION_CONFIG.js as `search_app_title` and `logo_location`. 106 | 107 | CSS options are configured in the files in client/assets/scss. 108 | 109 | Templates for various UI components are located in client/assets/components. 110 | 111 | Search results from different document types can use different templates. The `client/assets/components/document` directory contains templates for some common document types, plus default templates for all others. Data types correspond to Connectors in Fusion. See [Customizing Documents](docs/Customizing_Documents.md) for details about working with these. 112 | 113 | ## View on Windows 114 | 115 | Download the latest view installer from and run it **as an administrator**. 116 | 117 | ## What's Next 118 | 119 | For more details about configuring and customizing View, see the [docs](docs/) directory. 120 | 121 | ## Contributions 122 | 123 | View is open source! Pull requests welcome. This is a great way to give back to the community and help others build a better search app. 124 | --------------------------------------------------------------------------------