The articles in this section answer frequently asked questions about various aspects and functionality of App Studio.

More information

See the following how-to articles:
  • Adding Twitter and YouTube Results
  • Changing the Color Scheme
  • Changing the Logo
  • Dynamically Adding Icons to Fields
  • Removing the Security Module
  • Setting Up a Mock Platform Response
To add Twitter and Youtube results to the page, you must follow five steps.

1. Get the platform authentication keys

To get Twitter oAuth keys visit the Twitter dev site for Single User OAuth tokens. To get a Youtube API key visit the Google Developers Console, register an application and generate keys.

2. Add the dependencies of the YouTube and Twitter platform.

Add the following dependencies to the /pom.xml file at the root of your project
<dependency>
    <groupId>twigkit</groupId>
    <artifactId>twigkit.youtube</artifactId>
    <version>${project.parent.version}</version>
    <exclusions>
        <exclusion>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>twigkit</groupId>
    <artifactId>twigkit.twitter</artifactId>
    <version>${project.parent.version}</version>
</dependency>
If you are running a local server, then you must restart. Restarting the app-studio script pulls down any new library dependencies.

3. Add configuration files

Because both the Youtube and Twitter platforms take various oAuth and API keys, it is advised to keep these in centralized configuration files. Create these files:

src/main/resources/conf/platforms/twitter.conf

name: twigkit.search.twitter.Twitter
oAuthConsumerKey: XXXXXXXXXX
oAuthConsumerSecret: XXXXXXXXXX
oAuthAccessToken: XXXXXXXXXX
oAuthAccessSecret: XXXXXXXXXX

src/main/resources/conf/platforms/youtube.conf

name: twigkit.web.youtube.YouTube
apiKey: XXXXXXXXXX
channelId: XXXXXXXXXX

4. Configure platform, query and response in your View

Next configure a platform, query, and response at the top of the page, as you would do with a standard search. You can copy/paste the below:
<!-- YouTube -->
<search:platform var="youtubePlatform" conf="platforms.fusion.youtube"></search:platform>
<search:query var="youtubeQuery" parameters="*" results-per-page="12"></search:query>
<search:response var="youtubeResponse" platform="youtubePlatform" query="youtubeQuery"></search:response>
<!-- Twitter -->
<search:platform var="twitterPlatform" conf="platforms.fusion.twitter"></search:platform>
<search:query var="twitterQuery" parameters="*" results-per-page="12"></search:query>
<search:response var="twitterResponse" platform="twitterPlatform" query="twitterQuery"></search:response>
You can adjust the results-per-page to taste based on the number of results you want. Currently both queries are set to search based on the current query (parameters="*"). Set this to an empty string to return just the latest videos/tweets accordingly.

5. Show results on the page

Add the following code to the same page where the platform, query and response were defined in the previous step:
<!-- YouTube results -->
<search:result-list response="youtubeResponse">
    <search:result>
        <search:field name="snippet.title" styling="title"></search:field>
        <iframe width="400" height="300" id="player" ng-src="{{'https://www.youtube.com/embed/' + result.id }}" frameborder="0" ></iframe>
    </search:result>
</search:result-list>
<!-- Twitter results -->
<search:result-list response="twitterResponse">
    <search:result>
        <search:field name="text" styling="title"></search:field>
        <search:field name="user.screen_name" styling="description"></search:field>
    </search:result>
</search:result-list>
If embedding a YouTube video player like in the above example, you must also add the YouTube domain as a trusted site. To do this, add the following code to the /src/main/webapp/search/app/app.js file after the controller definitions:
.config(function($sceDelegateProvider) {
    $sceDelegateProvider.resourceUrlWhitelist([
        'self',
        'https://www.youtube.com/embed/**']);
});
After adding the above block, the entire /src/main/webapp/search/app/app.js file should look similar to the following:
'use strict';
const BUILD_PATH = '/dist/';
const basePath = (document.getElementsByTagName('base')[0] || {href: ''}).href;
__webpack_public_path__ = window.__webpack_public_path__ = basePath.replace(/\/$/, '') + BUILD_PATH;
import '../../styles/twigkit.less';
import {RoutesModule} from './routes/routes.module.js';
import {SearchController} from './controllers/search.controller';
import {LoginController} from "./controllers/login.controller";
import {ServicesModule} from "./services/services.module";
import {DirectivesModule} from "./directives/directives.module.js";
let appModule = angular
    .module('appStudioSearch', [
        , 'ui.router'
        , 'ngAnimate'
        , 'lightning'
        , RoutesModule.name
        , DirectivesModule.name
        , ServicesModule.name
    ])
    .run(['$rootScope', '$window', '$twigkit', '$location', function ($rootScope, $window, $twigkit, $location ) {
        $rootScope.$on('response_response_error', function (response) {
            $rootScope.showErrorModal = true;
        });
        $rootScope.closeErrorModal = function () {
            $rootScope.showErrorModal = false;
        }
        $rootScope.$on('httpInterceptorError', function (event, error) {
            /**
             * The tests here must match the tests in
             * twigkit.security.springsecurity.matchers.NonAjaxRequestMatcher::matches
             */
            if ((error.status == '401' || error.status == '403') &&
                (error.config.url.indexOf('twigkit/api') !== -1 ||
                    error.config.url.indexOf('twigkit/services') !== -1 ||
                    error.config.url.indexOf('twigkit/secure/services') !== -1 ||
                    error.config.url.indexOf('views/') !== -1
                )) {
{/*                 // Error was a 403 and URL was a non-user facing path - redirect to login */}
                $rootScope.$broadcast('twigkitApi403', { error: error, url: $location.url() });
                var loginPath = $twigkit.getContextPath('/') + $twigkit.getConstant('loginPage', 'login/');
{/*                 // If we are already on the login page, do not redirect */}
                if ($window.location.pathname !== loginPath ) {
                    $window.location.href = loginPath;
                }
            }
        });
    }])
    .controller('searchCtrl', SearchController)
    .controller('loginCtrl', LoginController)
    .config(function($sceDelegateProvider) {
        $sceDelegateProvider.resourceUrlWhitelist([
            'self',
            'https://www.youtube.com/embed/**']);
    });
angular.bootstrap(document, ['appStudioSearch']);
The principle colors used in the theme are define at the top of styles/includes/theme.less:
@color-primary: #273142; // Brand Primary

@color-secondary: #202020; // Utility Black (Non selectable)
@color-tertiary: #4CAF50; // Utility Green Links (Non selectable)
@color-quaternary: #f7f7f7; // Utility Grey page bg
@color-quinary: #d9d9d9; // Utility Grey borders

@color-white: #fff;
@color-black: #000;

@link-color: #498fe2;
Change the @color-primary, @color-secondary, and @color-tertiary variables to the desired color.For more precise control, the color of individual elements can be controlled in the styling partial stylesheets found in styles/includes/.

Dynamically Adding An Icon To A Field

Sometimes you might want to dynamically prepend a field with an image or an icon depending on the value of a field or variable.This can easily be accomplished using a class attribute referencing a little bit of CSS, defined in your styles/includes/custom.less.For example, to dynamically add a ‘type’ image before a field:
    [class*="field-icon-"] {
        position: relative;
        margin-left: 2em;
    }

    [class*="field-icon-"]:before {
        position: absolute;
        left: -30px;
        bottom: 14px;
        background-size: 20px 20px;
        display: inline-block;
        width: 20px;
        height: 20px;
        content: " ";
    }

    .field-icon-pdf:before {
        background-image: url('../assets/icon-pdf.png');
    }

    .field-icon-doc:before {
        background-image: url('../assets/icon-doc.png');
    }
And in your view, use:
<search:field name="type" ng-class="field-icon-{{result | fields:'type' | actual}}"></search:field>
The CSS can easily be extended or adapted to add more icon types or change the position and size of the icon.
These steps are required to remove the security module from a project.
You must also remove any other module that depends on the security module such as the Collaboration module.

1. POM

Remove the security module dependency (and any other module that requires security) from pom.xml:
<dependency>
    <groupId>twigkit</groupId>
    <artifactId>twigkit.security.provider.spring-security</artifactId>
    <version>${project.parent.version}</version>
</dependency>

2. Configuration

Replace the security module specified in conf/resources/security/security.conf with type: generic

3. Web.xml application descriptor

You must remove any reference to the Spring security filters and listeners from your web.xml application descriptor. For example:
<!-- Spring Security -->
<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:spring-security.xml</param-value>
</context-param>
<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

4. Java code

If you are using any classes from the security Module in Java code, remove references to these. For example, if the SecurityModule is installed in a custom module, you would remove these references:
import twigkit.security.springsecurity.SpringSecurityModule;

install(new SpringSecurityModule());

5. Security configuration files

Optionally, remove the spring-security.xml configuration file.
In some cases it can be difficult to access a search platform. In these situations, it can be useful to work against a static response in the form of a file containing a typical search response (usually as XML).This is not a recommended method for developing a fully featured search application from start to finish, because it is impossible to emulate the dynamic nature of a live platform. However this is useful in a number of situations including:
  • Where networking or security considerations prevent access to the platform
  • Debugging issues in an application where it is necessary to recreate a specific type of response
  • Initial development phases where access to a live platform with realistic data is unavailable
  • Deterministic test conditions (this could be for unit tests as well as any other form of manual or automated testing)

How to set up a mock platform

A few simple steps are required to run an application against a ‘mock’ response:
  1. Create a mock response file by saving the XML from a query issued directly against the platform, or request one from someone who can access a platform instance. This should represent the type of response you want to mock. See this example of a Solr XML response:
    <?xml version="1.0" encoding="UTF-8"?>
    <response>
        <lst name="responseHeader">
            <int name="status">0</int>
            <int name="QTime">24</int>
            <lst name="params">
            <str name="spellcheck">true</str>
            <str name="facet">true</str>
            <str name="indent">true</str>
            <str name="facet.limit">10</str>
            <str name="wt">xmk</str>
            <str name="rows">5</str>
            <str name="fl">*,score</str>
            <str name="start">0</str>
            <arr name="q">
                <str>*:*</str>
            </arr>
            <str name="q.op">OR</str>
            <str name="facet.field">month</str>
            <str name="qt">/prodover</str>
            <str name="fq">category:Clothing</str>
            </lst>
        </lst>
        <result name="response" numFound="21111" start="0" maxScore="1.0">
            <doc>
            <str name="category">Clothing</str>
            <str name="p_image_path_1_s">/s/c/sc1402sep26pictga10015.jpg</str>
            <int name="p_id_i">356980</int>
            <date name="p_date">2014-10-07T00:00:00Z</date>
            <str name="p_name">Generic T-Shirt</str>
            <float name="p_price">552.63</float>
            <arr name="colors">
                <str>Black</str>
                <str>White</str>
                <str>Green</str>
            </arr>
            <long name="_version_">1486360119259168768</long>
            <float name="score">1.0</float>
            </doc>
            <doc>
            <str name="category">Clothing</str>
            <str name="p_image_path_1_s">/s/c/sc1402sep26pictga10015.jpg</str>
            <int name="p_id_i">356980</int>
            <date name="p_date">2014-10-07T00:00:00Z</date>
            <str name="p_name">Generic T-Shirt</str>
            <float name="p_price">552.63</float>
            <arr name="colors">
                <str>Black</str>
                <str>White</str>
                <str>Green</str>
            </arr>
            <long name="_version_">1486360119259168768</long>
            <float name="score">1.0</float>
            </doc>
            <doc>
            <str name="category">Clothing</str>
            <str name="p_image_path_1_s">/s/c/sc1402sep26pictga10015.jpg</str>
            <int name="p_id_i">356980</int>
            <date name="p_date">2014-10-07T00:00:00Z</date>
            <str name="p_name">Generic T-Shirt</str>
            <float name="p_price">552.63</float>
            <arr name="colors">
                <str>Black</str>
                <str>White</str>
                <str>Green</str>
            </arr>
            <long name="_version_">1486360119259168768</long>
            <float name="score">1.0</float>
            </doc>
            <doc>
            <str name="category">Clothing</str>
            <str name="p_image_path_1_s">/s/c/sc1402sep26pictga10015.jpg</str>
            <int name="p_id_i">356980</int>
            <date name="p_date">2014-10-07T00:00:00Z</date>
            <str name="p_name">Generic T-Shirt</str>
            <float name="p_price">552.63</float>
            <arr name="colors">
                <str>Black</str>
                <str>White</str>
                <str>Green</str>
            </arr>
            <long name="_version_">1486360119259168768</long>
            <float name="score">1.0</float>
            </doc>
            <doc>
            <str name="category">Clothing</str>
            <str name="p_image_path_1_s">/s/c/sc1402sep26pictga10015.jpg</str>
            <int name="p_id_i">356980</int>
            <date name="p_date">2014-10-07T00:00:00Z</date>
            <str name="p_name">Generic T-Shirt</str>
            <float name="p_price">552.63</float>
            <arr name="colors">
                <str>Black</str>
                <str>White</str>
                <str>Green</str>
            </arr>
            <long name="_version_">1486360119259168768</long>
            <float name="score">1.0</float>
            </doc>
        </result>
        <lst name="facet_counts">
            <lst name="facet_queries"/>
            <lst name="facet_fields">
            <lst name="month">
                <int name="November">9140</int>
                <int name="October">7936</int>
                <int name="September">7215</int>
                <int name="August">7088</int>
                <int name="July">6878</int>
                <int name="June">5238</int>
                <int name="May">4351</int>
                <int name="April">4207</int>
                <int name="March">3593</int>
                <int name="February">3002</int>
            </lst>
            </lst>
            <lst name="facet_dates"/>
            <lst name="facet_ranges"/>
            <lst name="facet_intervals"/>
        </lst>
    </response>
    
  2. Copy the XML file into your project to:
    src/main/webapp/WEB-INF/pages/mock-response.xml
    
  3. Modify the URL rules configuration to add a rule that emulates the path at which the search platform interface exists. For example:
        <rule>
            <from>^/login/</from>
            <to last="true">/login.jsp</to>
        </rule>
    
        <rule>
            <from>^/login</from>
            <to last="true">/login.jsp</to>
        </rule>
    
        <rule match-type="wildcard">
            <name>Ignore files on URL path.</name>
            <from>/**.*</from>
            <to last="true">/$0</to>
        </rule>
        <rule>
            <from>^/mock-response/</from>
            <to last="true">/WEB-INF/pages/mock-response.xml</to>
        </rule>
        <rule match-type="wildcard">
            <name>HTML 5 URL Rewrite</name>
            <from>/**</from>
            <to last="true">/index.jsp</to>
        </rule>
    
  4. Configure the platform host to point to the relevant URL:
    name: twigkit.search.solr.Solr
    result-id-field: url
    host: http://localhost:8080/mock-response
    defaultQuery: test
    
The application is now configured to use this response XML file instead of a live platform whenever a query is issued. Obviously this will mean the same result set is retrieved no matter how the query is modified through the user interface. It is possible to mock different result sets by adding more complex URL rules to intercept the different parameters modified by interacting with Appkit UI components, however this is generally not recommended.