Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
dist/
/node_modules/
.DS_Store
.zat
3 changes: 3 additions & 0 deletions ZendeskClientApp/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.DS_Store
tmp
.zat
8 changes: 8 additions & 0 deletions ZendeskClientApp/.zat
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"zat_latest": "3.8.11",
"zat_update_check": "2022-06-13",
"subdomain": "d3v-algolia-peter",
"username": "[email protected]/token",
"app_id": 810216,
"password": "dAkiM1hu31vhyyzD9l68Nzu9aPvKvrMVCGhFMbz2"
}
26 changes: 26 additions & 0 deletions ZendeskClientApp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Related Articles

This is the Zendesk Ticket app. It has 2 views:
* ticket sidebar - displays related articles in relation to the ticket. The relation is matched with configurable attributes that are either tags, author, or subject
* nav bar - displays as an icon on the left of the ticket section, and displays all articles that are searchable and also has autocomplete

The app is configurable with settings that can be changed in the admin section of the app.

## Development

* First, install the zendesk App Tools (zat) [Zendesk Install](https://developer.zendesk.com/documentation/apps/zendesk-app-tools-zat/installing-and-using-the-zendesk-apps-tools/)

Once that's installed, go to the ZendeskClientApp folder in you terminal
```
cd ZendeskClientApp
```
Assuming you haven't installed this app in your Zendesk environment, you'll first do zat create
```
zat create
```
There are parameters in the manifest file that will prompt you for the correct values.

For updating the app after making any changes, do zat update
```
zat update
```
156 changes: 156 additions & 0 deletions ZendeskClientApp/assets/algoliaFunctions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
const client = ZAFClient.init();

const getSettings = async function() {
const metadata = await client.metadata();
return metadata.settings;
}

const getTicketDetails = async function() {
const ticketDetails = await client.get('ticket');
return ticketDetails;
}

const setupSearchWidgets = function(settings, search, sidebar) {
const virtualSearchBox = instantsearch.connectors.connectSearchBox(() => {})({});
const realSearchBox = instantsearch.widgets.searchBox({
container: '#searchbox',
});
const searchBox = (!sidebar && settings.useAutocomplete)? virtualSearchBox : realSearchBox;
const searchWidgets = [];
const ticketWidgets = [];

searchWidgets.push(searchBox);
if ( !sidebar ) {
searchWidgets.push( instantsearch.widgets.clearRefinements({
container: '#clear-refinements',
}));
}

if ( !sidebar && settings.articleFacet && settings.articleFacet.length > 0 ) {
searchWidgets.push(instantsearch.widgets.refinementList({
container: '#article-refinement',
attribute: settings.articleFacet,
}));
};
if ( settings.ticketFacet && settings.ticketFacet.length > 0 ) {
if (sidebar) {
ticketWidgets.push(instantsearch.widgets.menuSelect({
container: '#ticket-refinement',
attribute: settings.ticketFacet,
}));
} else {
ticketWidgets.push(instantsearch.widgets.refinementList({
container: '#ticket-refinement',
attribute: settings.ticketFacet,
}));
}
};

searchWidgets.push(instantsearch.widgets.hits({
container: '#hits',
templates: {
item(hit) {
return articleHit(hit);
}
}
}));

ticketWidgets.push(instantsearch.widgets.hits({
container: '#ticket-hits',
placeholder: 'Search Tickets',
templates: {
item(hit) {
return ticketHit(hit)
}
},
}));

searchWidgets.push(instantsearch.widgets
.index({ indexName: settings.ticketIndex })
.addWidgets(ticketWidgets));

searchWidgets.push(instantsearch.widgets.pagination({
container: '#pagination',
}));

search.addWidgets(searchWidgets);

search.start();
}

function debounce(fn, time) {
let timerId = undefined

return function(...args) {
if (timerId) {
clearTimeout(timerId)
}

timerId = setTimeout(() => fn(...args), time)
}
}

const truncate = (input) => input.length > 50 ? `${input.substring(0, 50)}...` : input;

const commentsSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" focusable="false" viewBox="0 0 12 12">
<path fill="none" stroke="currentColor" d="M1 .5h10c.3 0 .5.2.5.5v7c0 .3-.2.5-.5.5H6l-2.6 2.6c-.3.3-.9.1-.9-.4V8.5H1C.7 8.5.5 8.3.5 8V1C.5.7.7.5 1 .5z"></path>
</svg>`;

const articleHit = (hit) => {
let createdAt = moment(hit.created_at_iso).fromNow();

return `<div id="hit-component">
<div>${hit.section.full_path} ${createdAt}</div>
<div id="hit-component-subject">
<a href="https://d3v-algolia-peter.zendesk.com/hc/en-us/articles/${hit.id}" target="_blank">
<h3 class="ticket-title">${hit.title}</h3>
</a>
</div>
<div id="hit-component-description">
<p id="description" class="truncate"> ${hit.body_safe}</p>
</div>
<div class="btn-place-holder">
<button id="toggle-expand-ticket" onclick="expandToggle(this)">${expandToggleIcon}</button>
</div>
</div>`;
}

// eslint-disable-next-line no-unused-vars
function expandToggle(element) {
const descriptionElement = element.parentElement.parentElement.querySelector(
'#description'
);
const expandBtn = element;
expandBtn.classList.toggle('rotateicon180');
descriptionElement.classList.toggle('expanded-truncate');
}


const expandToggleIcon = `
<svg id="Component_23_1" data-name="Component 23 – 1" xmlns="http://www.w3.org/2000/svg" width="10" height="13" viewBox="0 0 10 13">
<rect id="Rectangle_1" data-name="Rectangle 1" width="4.308" height="7.818" transform="translate(2.832)" fill="#3f51b5"/>
<path id="Polygon_1" data-name="Polygon 1" d="M5,0l5,7H0Z" transform="translate(10 13) rotate(180)" fill="#3f51b5"/>
</svg>
`;

const ticketHit = (hit) => {

return `<div id="hit-component">
<div id="hit-component-subject">
<a href="${hit.ticketUrl}" target="_blank">
<h3 class="ticket-title">${hit.subject}</h3>
</a>
</div>
<div id="hit-component-description">
<p id="description" class="truncate"> ${hit.plain_body}</p>
</div>
<div class="btn-place-holder">
<button id="toggle-expand-ticket" onclick="expandToggle(this)">${expandToggleIcon}</button>
</div>
</div>`;

}




189 changes: 189 additions & 0 deletions ZendeskClientApp/assets/autocomplete.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
function setupAutocomplete(settings, searchClient, search, historyRouter) {

function setInstantSearchUiState(indexUiState) {
search.setUiState(uiState => ({
...uiState,
[settings.articleIndex]: {
...uiState[settings.articleIndex],
// We reset the page when the search state changes.
page: 1,
...indexUiState,
},
}));
}

// Return the InstantSearch index UI state.
function getInstantSearchUiState() {
const uiState = historyRouter.read()

return (uiState && uiState[settings.articleIndex]) || {}
}

// Build URLs that InstantSearch understands.
function getInstantSearchUrl(indexUiState) {
return search.createURL({ [settings.articleIndex]: indexUiState });
}

// Detect when an event is modified with a special key to let the browser
// trigger its default behavior.
function isModifierEvent(event) {
const isMiddleClick = event.button === 1;

return (
isMiddleClick ||
event.altKey ||
event.ctrlKey ||
event.metaKey ||
event.shiftKey
);
}

function onSelect({ setIsOpen, setQuery, event, query }) {
// You want to trigger the default browser behavior if the event is modified.
if (isModifierEvent(event)) {
return;
}

setQuery(query);
setIsOpen(false);
setInstantSearchUiState({ query });
}

function getItemUrl({ query }) {
return getInstantSearchUrl({ query });
}

function createItemWrapperTemplate({ query, children, html}) {
const uiState = { query };
return html`<a
class="aa-ItemLink"
href="${getInstantSearchUrl(uiState)}"
onClick="${(event) => {
if (!isModifierEvent(event)) {
// Bypass the original link behavior if there's no event modifier
// to set the InstantSearch UI state without reloading the page.
event.preventDefault();
}
}}"
>
${children}
</a>`;
}

const recentSearchesPlugin = createLocalStorageRecentSearchesPlugin({
key: 'instantsearch',
limit: 3,
transformSource({ source }) {
return {
...source,
getItemUrl({ item }) {
return getItemUrl({
query: item.label,
});
},
onSelect({ setIsOpen, setQuery, item, event }) {
onSelect({
setQuery,
setIsOpen,
event,
query: item.label,
});
},
// Update the default `item` template to wrap it with a link
// and plug it to the InstantSearch router.
templates: {
...source.templates,
item(params) {
const { children } = source.templates.item(params).props;

return createItemWrapperTemplate({
query: params.item.label,
children,
html: params.html,
});
},
},
};
},
});

const querySuggestionsPlugin = createQuerySuggestionsPlugin({
searchClient,
indexName: 'zendesk_d3v-algolia-peter_articles_query_suggestions',
getSearchParams() {
// This creates a shared `hitsPerPage` value once the duplicates
// between recent searches and Query Suggestions are removed.
return recentSearchesPlugin.data.getAlgoliaSearchParams({
hitsPerPage: 6,
});
},
transformSource({ source }) {
return {
...source,
sourceId: 'querySuggestionsPlugin',
getItemUrl({ item }) {
return getItemUrl({
query: item.name,
});
},
onSelect({ setIsOpen, setQuery, event, item }) {
onSelect({
setQuery,
setIsOpen,
event,
query: item.label,
});
},
getItems(params) {
// We don't display Query Suggestions when there's no query.
if (!params.state.query) {
return [];
}
return source.getItems(params);
},
templates: {
...source.templates,
item(params) {
const { children } = source.templates.item(params).props;
return createItemWrapperTemplate({
query: params.item.name,
children,
html: params.html,
});
},
},
};
},
});

const searchPageState = getInstantSearchUiState();

const debouncedSetInstantSearchUiState = debounce(setInstantSearchUiState, 500)

autocomplete({
container: '#autocomplete',
placeholder: 'Search for Articles',
detachedMediaQuery: 'none',
openOnFocus: true,
plugins: [recentSearchesPlugin,querySuggestionsPlugin],
initialState: {
query: searchPageState.query || '',
},
onSubmit({ state }) {
setInstantSearchUiState({ query: state.query })
},
onReset() {
setInstantSearchUiState({ query: '' })
},
onStateChange({ prevState, state }) {
if (prevState.query !== state.query) {
if ( settings.useDebounce ) {
debouncedSetInstantSearchUiState({ query: state.query })
} else {
setInstantSearchUiState({ query: state.query })
}
}
},
});
}

Loading