Appearance
Search Page ​
File Information ​
- Path:
app/pages/[brand_name]/search.vue - Route:
/{brand_name}/search - Purpose: Advanced search interface with Typesense integration, filters, and results display
- Layout:
collage-layout - Middleware:
check-url
Overview ​
The Search page provides comprehensive search functionality with Typesense integration. It supports full-text search, advanced filtering (tags, file types, dates), pagination, sorting, bulk actions, and URL-based search state management with encryption.
Key Features ​
Full-Text Search
- Search query input
- Typesense integration
- Search suggestions
- Encrypted URL state management
Advanced Filters
- Tag filters with infinite scroll pagination
- File type filters with pagination
- Date range filters (date-added, last-modified-date)
- Custom field filters
- Filter persistence in URL
Search Results
- Grid and list view modes
- Sortable columns (Name, Date Modified, Date Added, File Size, File Type)
- Infinite scroll pagination
- Bulk selection and actions
Bulk Actions
- Download selected assets
- Share selected assets
- Remove selection
URL State Management
- Encrypted query parameters
- Filter state in URL
- Deep linking support
- Browser back/forward support
Components Used ​
SearchFilter- Advanced filter panel (app/components/search/SearchFilter.vue)AddedFilterOption- Active filter display (app/components/search/AddedFilterOption.vue)SearchBulkActions- Bulk actions toolbar (app/components/search/SearchBulkActions.vue)SearchAssets- Results display component (app/components/search/SearchAssets.vue)SearchTableHeader- Table header with sorting (app/components/search/SearchTableHeader.vue)ViewToggle- Grid/list view toggle (app/components/miscellaneous/ViewToggle.vue)SearchListSkeleton- List view skeleton loaderSearchGridAssetsSkeleton- Grid view skeleton loaderAsyncIcon- Async icon component
Composables Used ​
useSearch()- Search functionality (app/composables/core/pages/useSearch.ts)useSearchStore()- Search state management (app/stores/search.ts)useDownloadStore()- Download storeuseSnackbar()- Snackbar notificationsuseCrypto()- Data encryption utilitiesuseFileType()- File type utilities
Data Properties ​
Reactive Data (from useSearch composable) ​
typescript
{
contentLoading: boolean, // Initial content loading
digitalAssets: SearchDigitalAsset[], // Search results
allFilterList: FilterItem[], // Active filters list
fileTypeList: any[], // Available file types (paginated)
tagsList: any[], // Available tags (paginated)
mainFilterObj: any, // Main filter object
loading: boolean, // Search loading state
loadMore: boolean, // Load more pagination state
assetsMode: 'grid' | 'list', // View mode
assetSortLoader: string, // Active sort column loader
assetsColumns: Column[], // Table column definitions
assetsSortActiveColumn: string, // Active sort column
assetsSortOder: boolean, // Sort order (asc/desc)
assetSortingState: SortingState, // Complete sorting state
assetsSelectedItems: DigitalAsset[], // Selected assets
assetSelectedCount: number, // Selected count
downloadSlctdAssets: boolean, // Download in progress
resultsCount: number, // Total results count
searchNotAllowed: boolean, // Search not allowed flag
lastIndexAssets: number, // Last selected index
tagsPagination: TagPagination, // Tags pagination state
fileTypePagination: TagPagination, // File types pagination state
noDataFound: boolean // No results flag
}Column Definitions ​
typescript
[
{ key: "check", label: "", minWidthClass: "check", sortingShow: false },
{ key: "display_file_name", label: "Name", minWidthClass: "Name", sortingShow: true },
{ key: "modified_at", label: "Date Modified", minWidthClass: "minw130", sortingShow: true },
{ key: "created_at", label: "Date Added", minWidthClass: "minw130", sortingShow: true },
{ key: "file_size", label: "File Size", minWidthClass: "minw130", sortingShow: true },
{ key: "file_type", label: "File Type", minWidthClass: "minw130", sortingShow: true }
]Computed Properties ​
searchParams ​
Returns search query from store.
typescript
computed(() => store.searchKey)noDigitalAssetsResults ​
Returns true if no search results.
typescript
computed(() => digitalAssets.value.length === 0)advancedSearchAllowed ​
Returns true if user has advanced search subscription feature.
typescript
computed(() =>
user.subscription_features?.advance_search_functionality?.enable
)shouldShowNoData ​
Determines if "no data" message should be shown.
typescript
computed(() => {
if (loading.value || loadMore.value) return false
return noDigitalAssetsResults.value
})tagsList ​
Returns paginated tags list from search data.
typescript
computed(() => {
if (
searchData.value &&
searchData.value.popular_tag_select &&
searchData.value.popular_tag_select.length
) {
return searchData.value.popular_tag_select.slice(
tagsPagination.value.start,
tagsPagination.value.end
)
}
return []
})fileTypeList ​
Returns paginated file type list from search data.
typescript
computed(() => {
if (
searchData.value &&
searchData.value.popular_file_type &&
searchData.value.popular_file_type.length
) {
return searchData.value.popular_file_type.slice(
fileTypePagination.value.start,
fileTypePagination.value.end
)
}
return []
})tagsPaginationEnd ​
Returns true if tags pagination has reached end.
typescript
computed(() =>
tagsList.value.length >=
(searchData.value?.popular_tag_select?.length || 0)
)fileTypePaginationEnd ​
Returns true if file type pagination has reached end.
typescript
computed(() =>
fileTypeList.value.length >=
(searchData.value?.popular_file_type?.length || 0)
)noDataFound ​
Returns true if no data should be displayed.
typescript
computed(() =>
!loading.value &&
!loadMore.value &&
shouldShowNoData.value &&
!contentLoading.value
)Methods ​
initOps(flag = "default") ​
Initializes search operations and processes URL query.
typescript
async function initOps(flag = "default") {
try {
if (flag === "default") {
let redirectHome = flag === "default"
// Decrypt and process URL query
if (route.query.q) {
try {
const data = await decryptData(route.query.q as string)
const tempSearch = data.searchKey || ""
const pageType = data.pageType || ""
store.updateSearchKey(tempSearch)
store.updateAllFilterList(data.filter || [])
allFilterList.value = cloneDeep(data.filter || [])
initPageTypeFilter(pageType)
redirectHome = false
} catch (error) {
console.error(error)
snackbar.showError(error)
}
}
if (redirectHome) {
snackbar.showError("Invalid URL")
return await navigateTo(`/${getBrandName()}`)
}
}
allFilterList.value = cloneDeep(store.allFilterList)
await nextTick()
await fetchInitTags()
await fetchResults("search")
contentLoading.value = false
} catch (error) {
console.error("Error in initOps:", error)
contentLoading.value = false
}
}Flow:
- Decrypts URL query if present
- Updates store with search key and filters
- Initializes page type filter
- Fetches initial tags
- Fetches search results
- Handles errors gracefully
initPageTypeFilter(value: string) ​
Initializes file type filter from page type.
typescript
function initPageTypeFilter(value: string) {
if (value === "audios") {
assetTypeFilter.value = `(type:='audio')`
} else if (value === "images") {
assetTypeFilter.value = `(type:='image')`
} else if (value === "documents") {
assetTypeFilter.value = `(type:='application')`
} else if (value === "videos") {
assetTypeFilter.value = `(type:='video')`
} else {
assetTypeFilter.value = ""
}
}fetchInitTags(flag = "default") ​
Fetches and processes initial tag and file type data.
typescript
async function fetchInitTags(flag = "default") {
if (flag === "default") {
searchData.value = cloneDeep(store.popularSearchData)
initOrigIndexCache(false)
// Move selected filters beyond cutoff to front
if (allFilterList.value.length) {
// Complex logic to reorder selected items in tag/file type lists
// Ensures selected items are visible even if beyond pagination cutoff
}
}
await fetchSearchData()
}Purpose: Ensures selected filters are visible in dropdowns even if they're beyond the initial 49 items.
fetchSearchData() ​
Fetches search metadata (tags and file types).
typescript
async function fetchSearchData() {
try {
const response: any = await getSearchData(workspaceId.value)
if (response.status) {
let { tags, file_type } = response.data
const newFileType = file_type.map((type: any) => ({
name: type,
id: type,
}))
searchData.value = {
...searchData.value,
popular_tag_select: tags,
popular_file_type: newFileType,
}
const storeSearchObj = cloneDeep(searchData.value)
store.updatePopularSearchData(storeSearchObj)
}
} catch (error) {
console.error("Error fetching search data:", error)
}
}fetchResults(flag = "filter") ​
Fetches search results from Typesense.
typescript
async function fetchResults(flag = "filter") {
loading.value = flag === "filter"
loadMore.value = flag === "scroll"
if (flag === "filter") {
allFilterList.value = cloneDeep(store.allFilterList)
if (route.name === "brand_name-search") {
await updateSearchPageUrl()
}
}
let search = searchParams.value.trim()
searchNotAllowed.value = false
const emptySearch = checkReturnSearch(search)
if (flag === "all") {
search = "*" // Wildcard search
}
// Build request payload
const collectionFilters = getCollectionFilters(assetTypeFilter.value)
const sortParams = getAssetSortParams()
const currentPagination = pagination.value.assets
const pageNum = flag === "scroll" ? currentPagination.currentPage : 1
const requestPayload = {
request: {
q: search || "*",
collections: Object.keys(collectionFilters),
sort_by: sortParams,
},
filterQuery: collectionFilters,
commonSearchParams: {
query_by: "search_name",
per_page: perPage.value,
page: pageNum,
},
workspace_id_filter: workspaceId.value,
}
// Perform search
const searchResults = await searchAssetsApi(requestPayload)
// Process results
if (flag === "scroll") {
// Append new results
digitalAssets.value.push(...searchResults.digital_assets?.data.hits || [])
} else {
// Replace results
digitalAssets.value = searchResults.digital_assets?.data.hits || []
pagination.value.assets.totalPages = Math.ceil(
(searchResults.digital_assets?.data.found || 0) / perPage.value
)
}
}Parameters:
flag: string- Operation flag: "filter", "scroll", "all", "search"
Flow:
- Sets loading states
- Updates URL if filter changed
- Builds search query and filters
- Calls Typesense API
- Processes and updates results
- Updates pagination
handleScroll(event: Event) ​
Handles scroll events for infinite scroll pagination.
typescript
function handleScroll(event: Event) {
if (!import.meta.client) return
// Throttle using requestAnimationFrame
if (scrollRafId.value !== null) {
cancelAnimationFrame(scrollRafId.value)
}
scrollRafId.value = requestAnimationFrame(() => {
const container = event.target as HTMLElement
const scrollTop = container.scrollTop
const containerHeight = container.scrollHeight - container.clientHeight
const scrollPercent = (scrollTop / containerHeight) * 100
if (scrollPercent >= 60) {
if (debounceTimeout.value) {
clearTimeout(debounceTimeout.value)
}
debounceTimeout.value = setTimeout(() => {
loadMoreResults()
}, 300)
}
scrollRafId.value = null
})
}Performance:
- Uses
requestAnimationFramefor throttling - Debounces load more calls (300ms)
- Triggers at 60% scroll position
loadMoreResults() ​
Loads more search results for pagination.
typescript
async function loadMoreResults() {
const currentPagination = pagination.value.assets
if (
currentPagination.currentPage >= currentPagination.totalPages ||
loadMore.value ||
loading.value
) {
return
}
currentPagination.currentPage += 1
await fetchResults("scroll")
}updateSearchPageUrl() ​
Updates URL with encrypted search state.
typescript
const updateSearchPageUrl = async () => {
try {
if (route.name !== "brand_name-search" || isUpdatingUrl.value) return
const queryData = {
searchKey: searchParams.value || null,
filter: allFilterList.value,
}
const encrypted = await encryptData(queryData)
if (route.query.q !== encrypted) {
isUpdatingUrl.value = true
await router.replace({
name: "brand_name-search",
params: { brand_name: getBrandName() },
query: { q: encrypted },
})
setTimeout(() => {
isUpdatingUrl.value = false
}, 100)
}
} catch (error) {
console.error("Error updating search page URL:", error)
isUpdatingUrl.value = false
}
}Purpose: Maintains search state in URL for sharing and browser navigation.
handleSortAssets(column: string) ​
Handles column sorting.
typescript
function handleSortAssets(column: string) {
// Updates sort state and fetches results
assetsSortActiveColumn.value = column
assetsSortOder.value = !assetsSortOder.value
sortAssets()
}sortAssets() ​
Applies sorting and fetches results.
typescript
async function sortAssets() {
assetSortLoader.value = assetsSortActiveColumn.value
await fetchResults("sort")
assetSortLoader.value = ""
}updateTabMode(mode: 'grid' | 'list') ​
Updates view mode and caches preference.
typescript
function updateTabMode(mode: 'grid' | 'list') {
assetsMode.value = mode
setCachedViewMode(mode)
}UpdateSelectedAssets(items: DigitalAsset[]) ​
Updates selected assets list.
typescript
function UpdateSelectedAssets(items: DigitalAsset[]) {
assetsSelectedItems.value = items
assetSelectedCount.value = items.length
}assetsRemoveSelection() ​
Clears all selections.
typescript
function assetsRemoveSelection() {
digitalAssets.value = digitalAssets.value.map((obj) => ({
...obj,
document: {
...obj.document,
is_selected: false,
},
}))
assetsSelectedItems.value = []
assetSelectedCount.value = 0
}assetsDownloadFile() ​
Downloads selected assets.
typescript
async function assetsDownloadFile() {
downloadSlctdAssets.value = true
// Download logic
downloadSlctdAssets.value = false
}assetsAddToShare() ​
Opens share dialog for selected assets.
typescript
function assetsAddToShare() {
// Share logic
}removeAssets(assetId: number) ​
Removes asset from results (if needed).
typescript
function removeAssets(assetId: number) {
digitalAssets.value = digitalAssets.value.filter(
(asset) => asset.document.id !== assetId
)
}Filter Management Methods ​
removeFilterOptn(filter: FilterItem) ​
Removes a filter from active filters.
updateFilterObj(filterObj: any) ​
Updates filter object.
onUpdateFilter(filter: FilterItem) ​
Handles filter update event.
removeNullFilter() ​
Removes null/empty filters.
updateFilterListData(filterList: FilterItem[]) ​
Updates filter list data.
resetFilter() ​
Resets all filters.
User Interactions ​
Search Input
- Enter search query
- Search triggers on filter changes
- Encrypted URL state management
Filter Management
- Add filters (tags, file types, dates)
- Remove filters
- Reset all filters
- Filter pagination with infinite scroll
Results Display
- Toggle grid/list view
- Sort by columns
- Scroll for more results
- Select assets for bulk actions
Bulk Actions
- Download selected assets
- Share selected assets
- Remove selection
URL Sharing
- Share URL with search state
- Deep linking support
- Browser back/forward support
Real-time Features ​
- Search state encrypted in URL for security
- Filter state persists in URL
- Real-time search results from Typesense
Styling ​
- Uses custom CSS from
assets/scss/custom.scss - Responsive breakpoints for mobile/tablet/desktop
- Skeleton loaders for loading states
- Filter panel styling
- Table/list view styling
- Custom scrollbar styling
Watchers ​
Route Query Watcher ​
Watches route query changes to reload search.
typescript
watch(
() => route.query.q,
async (newQuery) => {
if (isUpdatingUrl.value) return
if (newQuery) {
await initOps("default")
}
},
{ immediate: false }
)Filter List Watcher ​
Watches filter list changes to update URL.
typescript
watch(
() => allFilterList.value,
async () => {
if (filterWatchActive.value && route.name === "brand_name-search") {
await updateSearchPageUrl()
}
},
{ deep: true }
)Lifecycle Hooks ​
onMounted ​
Initializes search on component mount.
typescript
onMounted(async () => {
await initOps("default")
})onUnmounted ​
Cleans up timers and event listeners.
typescript
onUnmounted(() => {
if (debounceTimeout.value) {
clearTimeout(debounceTimeout.value)
}
if (scrollRafId.value !== null) {
cancelAnimationFrame(scrollRafId.value)
}
})API Endpoints ​
Search Assets ​
- Endpoint:
POST /api/typesense/search(viasearchAssetsApi()) - Request: Typesense search payload with filters and sorting
- Response: Search results with pagination
Get Search Data ​
- Endpoint:
GET /search/data(viagetSearchData()) - Response: Tags and file types metadata
User Flow ​
- User navigates to search page (with or without query)
- Page decrypts URL query if present
- Initializes filters and search data
- Fetches search results
- User can:
- Enter search query
- Apply filters (tags, file types, dates)
- Sort by columns
- Toggle grid/list view
- Select assets for bulk actions
- Scroll for more results
- URL updates with encrypted state
- User can share URL with search state
Performance Optimizations ​
- Scroll Throttling: Uses
requestAnimationFramefor scroll events - Debouncing: Debounces load more calls (300ms)
- Pagination: Loads 48 items per page
- Lazy Loading: Tags and file types loaded with pagination
- URL State: Encrypted state prevents URL length issues
- Request Batching: Batches state updates using
nextTick
Notes for Development ​
- Search state is encrypted in URL for security
- Filter pagination ensures selected items are visible
- Infinite scroll triggers at 60% scroll position
- Sort state persists across filter changes
- View mode preference is cached
- Bulk actions work with selected items
- URL state enables deep linking and sharing
Related Documentation ​
- useSearch Composable - Search composable details
- Search Store - Search state management
- Typesense API - Search API endpoints
- SearchFilter Component - Filter component
- SearchAssets Component - Results component