Recipe: Sidebar Search app for App Builder | The place for Zendesk users to come together and share
Skip to main content

Recipe: Sidebar Search app for App Builder

  • June 30, 2026
  • 0 replies
  • 46 views

Vishal Jethani

Problem Statement

The Sidebar Search app is a ticket sidebar application that provides in-context search functionality directly within the ticket view. It allows agents to search across tickets, articles, comments, users, and organizations in their Zendesk without leaving the active ticket, with support for both basic keyword search and advanced filters such as ticket status, date ranges, and assignee.

 

Locations: ticket sidebar, new ticket sidebar

 

Prompt

Create a ticket_sidebar app called "Sidebar Search" that lets agents search their Zendesk Support instance for tickets, users, and organizations directly from the ticket sidebar, with both basic and advanced search options including status filters, date ranges, assignee filtering, and brand filtering.

 

Add two settings to the app so admins can configure them during installation:

- `custom_fields` — labelled "Custom field IDs". Help text: "If you would like the app to suggest the value of custom ticket fields, specify their IDs here, separated by commas." Required: no, type: text.

- `related_tickets` — labelled "Show keywords from subject as search suggestion?". Type: checkbox.

 

Read these settings using `const metadata = await zafClient.metadata()` then access `metadata.settings.custom_fields` and `metadata.settings.related_tickets`. **Do NOT use `zafClient.get('setting.*')` — that path does not exist and will throw `APIUnavailable` at runtime.**

 

**Initialization**

1. On load, fetch the current user's locale via `zafClient.get('currentUser')` and the current ticket data via `zafClient.get('ticket')`. Use conditional rendering (`{isLoading ? <Spinner /> : <SearchForm />}`) so no container persists after init.

2. Fetch all available brands from `/api/v2/brands.json?page[size]=100` using cursor-based pagination (follow `links.next` while `meta.has_more` is true, up to 100 pages max). Pre-select the brand matching the current ticket's brand ID.

3. Build search suggestions — always generate from the ticket subject regardless of settings:

   - Fetch subject via `zafClient.get('ticket.subject')`.

   - Convert to lowercase, strip punctuation (`/[.,-/#!$?%^&*;:{}=\-_~()]/g`), collapse spaces, split by spaces.

   - Filter out stopwords: "a", "is", "the", "and", "or", "for", "to", "in", "of", "it", "on", "at", "by", "an", "be", "do", "no", "so", "up", "if", "my", "as", "we", "he", "i", "am", "are", "was", "has", "had", "not", "but", "can", "all", "its", "you", "our", "out", "new", "one", "may", "way".

   - Additionally, if `metadata.settings.custom_fields` is set, parse numeric IDs and fetch custom field values via `zafClient.get('ticket.customField:custom_field_{id}')`. Add non-empty values.

   - Store in state: `setSuggestions([...suggestionsSet])`.

 

**Search Form UI**

4. Display a search form. Do NOT add `margin-top` or `padding-top` to the form or its first child.

   - **Search type fieldset** — label "Search for:" (block-level, 12px, font-weight 600) on its own line. Below it, a full-width `<select>` with options: "everything" (`all`, default), "tickets" (`ticket`), "people" (`user`), "organizations" (`organization`).

   - **Search input row** — a flex container styled as a bordered input (border-radius 4px, height 32px, 5px right padding). Inside: a bare text input (flex:1, 10px left padding, placeholder "Type search term here", required). On the right: a 30x30px submit button with a 16px search SVG icon. When searching, add `is-loading` class to the button (hides SVG, shows a CSS spinner animation).

   - **Suggestions row** — render directly in JSX (not conditionally):

     ```jsx

     <div style={{ marginBottom: '8px', overflow: 'visible' }}>

       <span className="suggestions-label">Suggestions: </span>

       {suggestions.map((word, i) => (

         <span key={i} onClick={() => { setKeyword(word); doSearch(word); }} className="suggestion-tag">

           {word}

         </span>

       ))}

     </div>

   - **Advanced/Basic toggle** — right-aligned blue link. "Advanced" when unchecked, "Basic" when checked. **When toggling from Advanced back to Basic, clear ALL advanced filter state:** reset selected statuses to empty, reset date range to "None", clear start/end date values, reset assignee to default "-", and reset brand to default "All brands". Then call resize.

 

5. **Advanced options section** (hidden by default, shown when toggle is checked):

   - **Ticket status multi-select** (only rendered when `searchType === 'ticket'`) — a multi-select input with removable tags. Options: New, Open, Pending, On-hold, Solved, Closed. Selected values appear as pill tags with × to remove.

   - **Date Range filter** — a `<select>` with options: "None" (default), "Created", "Updated". When a date type is selected, show "Start" and "End" date inputs side by side.

   - **Assignee dropdown** (only rendered when `searchType === 'ticket'`) — a `<select>` with "-" default followed by assignable agent names. Fetch from `/api/v2/group_memberships/assignable.json?include=users&page[size]=100` (cursor-based pagination, up to 100 pages).

   - **Brand filter dropdown** (always visible in advanced section) — a `<select>` with "All brands" default followed by brand names from step 2. Pre-select current ticket's brand.

 

6. **Ticket-only field visibility:** "Ticket status" and "Assignee" must ONLY render when `searchType === 'ticket'`. Use conditional rendering: `{searchType === 'ticket' && ( ... )}`. On initial load (default `"all"`), these are not in the DOM.

 

**Search Execution**

7. On form submit (search button, Enter, or suggestion click), build a query string:

   - If type is not "all": prepend `type:{selectedType}`

   - Append the keyword

   - If advanced is visible and type is "tickets": append `status:{value}` for each selected status, `assignee:"{name}"` if set

   - If date range selected: append `{range}>{fromDate}` and/or `{range}<{toDate}`

   - If brand selected: append `brand_id:"{brandId}"`

 

8. Call `GET /api/v2/search.json?per_page=5&query={encodedQuery}&page={pageIndex}` via `zafClient.request({ url, cors: true })`.

 

**Results Display**

9. Show results count: "No results found." for 0, "1 result" for 1, "{count} results" for 2+.

 

10. Results table: `width: 100%`, `border-collapse: collapse`. Each `<tr>` has `border-bottom: 1px solid` (use theme colour). Each `<td>` has `padding: 10px 8px`. Left cell: entity link. Right cell: type label, right-aligned.

 

11. Ticket results: clickable link "#ID Subject". On row hover, show a tooltip with truncated description (140 chars), positioned absolutely at `left: 115px, top: 50%, transform: translateY(-50%)`, 200px wide, with left-pointing arrow. Last row tooltip anchors to bottom.

 

12. User/organization results: clickable name link. Group results: plain link.

 

13. Clicking any result: `zafClient.invoke('routeTo', entityType, entityId)`.

 

**Pagination**

14. Show pagination when `next_page` or `previous_page` is non-null. Total pages: `Math.ceil(count / 5)`.

    Render as `<ul class="c-pagination" role="navigation">` with `<li>` items:

    - Previous "‹": clickable when `currentPage > 1`, `data-index={currentPage - 1}`.

    - Up to 7 page numbers. Current page: blue chip (`#1f73b7` background, white text, `aria-current="true"`). Others: blue text with `data-index`. Gaps shown as "N..." between visible pages.

    - Next "›": clickable when `currentPage < totalPages`, `data-index={currentPage + 1}`.

 

    Click handler: event delegation on the `<ul>`, read `data-index` from closest `<li>`, re-run search at that page.

 

**Error Handling**

15. On API failure, show an error callout with "An error occurred." title and the error message from the response.

 

**Styling**

16. **Borders:** All inputs/selects use `1px solid #c2c8cc` (light) / `#49545c` (dark), border-radius 4px.

17. **Font sizes:** Base 14px. Labels: 12px bold. Inputs: 14px. Suggestions: 11px bold. Toggle/result-type: 13px.

18. **Resize:** Create a helper that calls `zafClient.invoke('resize', { height: Math.min(document.body.clientHeight, 1000) })` inside `setTimeout(() => {}, 0)`. Trigger via `useEffect` watching `[searchType, showAdvanced, results, currentPage]` and on window resize (debounced 150ms). CSS: `html, body, #root { height: auto !important; min-height: 0 !important; overflow: hidden; }`.

19. **Suggestions CSS (light + dark):**

    ```css

    .suggestions-label { color: #2f3941; }

    .suggestion-tag {

      display: inline-block; background: #e9ebed; border-radius: 2px;

      padding: 1px 6px; font-size: 11px; font-weight: 600;

      margin-right: 6px; margin-bottom: 4px; cursor: pointer; color: #2f3941;

    }

    [data-theme="dark"] .suggestions-label { color: #d8dcde; }

    [data-theme="dark"] .suggestion-tag { background: rgba(255,255,255,0.12); color: #fff; }

20. **Date input theming:**

    ```css

    input[type="date"] { color: #2f3941; }

    [data-theme="dark"] input[type="date"] { color-scheme: dark; color: #d8dcde; }

    [data-theme="dark"] input[type="date"]::-webkit-calendar-picker-indicator { filter: invert(1); }

21. **Dark mode:** Listen for ZAF `colorScheme.changed` event. When dark, set `data-theme="dark"` on `<html>`. Overrides:

    - All backgrounds: `transparent`

    - All inputs/selects background: `transparent`

    - Text: `#d8dcde`

    - Borders: `#49545c`

    - Links: `#5293c7`

    - Select text: `#d8dcde`

    - Table row borders: `#49545c`

    - Pagination current: `#1f73b7` bg, white text

 

22. **No extra space:** No `margin-bottom` or `padding-bottom` on results wrapper or table. Always resize after rendering results.

 

 

Enhancement Options

Based on your organizational needs, you can extend the Sidebar Search app to support your specific needs using App Builder.

 

Important Note: This app recipe is for the Sidebar Search app. Any modifications to the recipe or its underlying logic may alter the core functionality of the app. Thoroughly test all changes in a non-production environment before deploying to your production workspace.

Please note: that as App Builder evolves including updates to underlying AI models, design components, and other dependencies, we cannot guarantee that this recipe will continue to function as expected over time. We recommend periodically validating the recipe against any platform updates.