Appearance
Collages ​
Overview ​
Collages are curated, shareable collections of assets. Each collage has its own name, description, visibility setting, and a multi-image mosaic thumbnail built from its first three preview images.
- Collage List —
pages/[workspace_id]/dam/collage/index.vuerenders an infinite-scroll grid or list view of all workspace collages usinguseCollageListQuery. - Collage Detail —
pages/[workspace_id]/dam/collage/[id]/index.vueshows the assets within a single collage with full search and filter support viauseCollageDetailsApi. - Collage Card —
components/collage/Card.vuerenders the mosaic thumbnail with up to three preview images in a 60/40 split layout. Options menu supports rename, share, download, permissions, and delete. - Collage List Item —
components/collage/ListItem.vuerenders a single collage row in table view with thumbnail, description, dates, asset count, and options. - Creation Flow —
dialogs/CreateOrRenameDialog.vuecollects name and description;useCollectionApi.createCollectionPOSTs to the backend. - Mutations —
useCollageListMutationswraps rename and delete with optimistic cache patches so the list updates instantly without a round trip.
Architecture ​
Collages are backed by the digital-assets/collection API resource. The list is fetched via TanStack infinite query (paginated server-side, sorted by modified_at DESC by default). Each page result is augmented with a uuid on every preview_image entry for stable Vue keying. The detail view uses useCollageContentsQuery which wraps useCollectionApi.getCollageDetails and loadCollageAssets in TanStack fetchQuery calls so the data is cached per collection ID. Rename mutations patch both the infinite-query pages cache and the dashboard recents cache directly — no refetch.
File Structure ​
Vue Component Files ​
app/components/collage/Card.vue— card view of a collage; renders 1-image, 2-image, or 3-image mosaic layouts with lazy loading and eager loading for the first four cards; includes options menu and rename dialogapp/components/collage/ListItem.vue— table row for a collage in list view; shows thumbnail, name, description (truncated with tooltip), modified date, created date, asset count, lock indicator, and options menuapp/components/collage/MediaCard.vue— media preview card used within collage detailapp/components/collage/MediaControls.vue— playback controls overlay for video assets in collage detailapp/components/collage/VideoControls.vue— video-specific playback controlsapp/components/collage/PreviewModal.vue— full-screen asset preview within the collage contextapp/components/collage/FolderCard.vue— folder card variant for use inside collage detailapp/components/collage/RecentFolderListItem.vue— recent-folder list row used in the dashboard recents widgetapp/components/collage/RecentUploadListItem.vue— recent-upload list row used in the dashboard recents widgetapp/components/dialogs/CreateOrRenameDialog.vue— shared dialog for creating a new collage or renaming an existing one; supports optional description fieldapp/components/dialogs/SaveToCollage.vue— "Add to collage" dialog for adding selected assets to an existing or new collage
Composable Files ​
app/composables/api/useCollectionApi.ts— all CRUD operations on the collection (collage) resource: list, detail, create, rename, delete, share, add/remove assetsapp/composables/api/useCollageDetailsApi.ts— detail-page composable combininguseSearchApi.getSearchResultsanduseCommonApis.getInnerSearchDatafor the in-collage search and filter poolapp/composables/queries/useCollageListQuery.ts— TanStack infinite query for the collage list; handles pagination, sorting, and preview-image uuid augmentationapp/composables/queries/useCollageContentsQuery.ts— TanStack fetch-query wrapper for collage detail metadata and paginated asset pages; exposesinvalidateContentsfor post-mutation invalidationapp/composables/queries/useCollageListMutations.ts— TanStack mutations for rename (optimistic cache patch) and delete (list invalidation)
Page Files ​
app/pages/[workspace_id]/dam/collage/index.vue— collage list page with sort controls, view mode toggle (grid/list), and infinite scrollapp/pages/[workspace_id]/dam/collage/[id]/index.vue— collage detail page with in-collage search, filter panel, tab switching between collage assets and sub-folders, bulk actions, and permission/share dialogs
Routes:
/:workspace_id/dam/collage— collage list/:workspace_id/dam/collage/:id— collage detail
Type Files ​
app/types/collection.ts—Collection,CollectionDetailsParams,CollectionApiResponseapp/types/dashboard.ts—Collage(includespreview_images,assets_count,name,description,instance_visibility)
Page Details ​
Collage List ([workspace_id]/dam/collage/index.vue) ​
| Field | Value |
|---|---|
| Route | /:workspace_id/dam/collage |
| Layout | collage-layout |
| Middleware | auth-check, check-workspace, check-if-suspended, can-access-dam-module |
| Primary composables | useCollage(), useCollageMenu() |
Grid and list view modes with infinite scroll and sortable columns. Permission-gated "New Collage" button. Empty state with CTA.
List columns: Name, Description (tooltip), Date Modified, Date Added, #Assets
Dialogs: ShareAssetDialog, ConfirmationDialog (delete), PermissionDialog (flag="collage"), CreateOrRenameDialog (rename)
Collage Detail ([workspace_id]/dam/collage/[id]/index.vue) ​
| Field | Value |
|---|---|
| Route | /:workspace_id/dam/collage/:id |
| Layout | collage-layout (embedsHeader: true) |
| Middleware | auth-check, check-workspace, check-if-suspended, can-access-dam-module |
| Primary composables | useCollageDetails(), useMarqueeSelection(), useSearchFiltersQuery(), useThumbnailSize('collage') |
Three view modes — Grid, List (DamDetailListView), Mosaic — all with infinite scroll. In-collage text search, filter panel (excludes the "Collage" category), select-all with cross-page confirmation, floating bulk-action bar, title ⋮ menu (Share/Download/Edit/Portals/Delete), and marquee multi-select.
List columns: Name, Date Modified, Date Added, File Size, File Type
Bulk actions (permission-gated): Add to Another Collage, Share, Download, Add Tags, Insert Custom Field
Per-asset menu: Share, Download, Rename, Portals, Remove From Collage
Dialogs: ConfirmationDialog, ShareAssetDialog, CreateOrRenameDialog, PermissionDialog, SaveToCollage, AddMultipleTags, ManageCustomFields
CollageCard Component ​
File: app/components/collage/Card.vue
Renders a collage as a mosaic thumbnail card. Preview images are displayed in three layouts: single image (full width), two images (60%/40% split), three or more images (60% left, 40% right split into two 50% rows). SVG and non-CDN images use an <img> tag; all others use Vuetify v-img with lazy loading. The first four cards in the list use loading="eager" and fetchpriority="high" for LCP optimization.
Features ​
- Dynamic mosaic layout based on
preview_images.length(1, 2, or 3+) - Eager loading for cards at index 0-3;
fetchpriority="high"for cards at 0-1 - Rename dialog via
CreateOrRenameDialogwith name + description fields - Options menu (Share, Download, Edit, Portals, Delete) — items conditionally visible per permission props
- Lock icon tooltip when
instance_visibilityindicates restricted access - Delegates behavior to
useCollageCardcomposable for menu items and image class computation
Props ​
typescript
interface CollageProps {
collage: {
id: number
name: string
collection_name?: string
description?: string
assets_count: number
preview_images?: Array<{ url?: string; uuid?: string; file_type?: string }>
instance_visibility?: unknown
[key: string]: unknown
}
downloading?: boolean // default: false — shows download spinner
flag?: string // 'search-page' to use search-page lock icon behavior
noDownload?: boolean // default: false — hides download menu item
isEdit?: boolean // default: false
index?: number // default: 0 — drives eager load threshold
}Events ​
typescript
{
edit: []
delete: [collage: unknown]
share: [collage: unknown]
permission: [collage: unknown]
nameChange: [payload: { id: number; name: string; description: string }]
download: []
}Usage ​
vue
<template>
<CollageCard
v-for="(collage, i) in collages"
:key="collage.id"
:collage="collage"
:index="i"
@share="onShare"
@delete="onDelete"
@name-change="onRename"
/>
</template>
<script setup lang="ts">
import type { Collage } from '~/types/dashboard'
const { rawCollages: collages } = useCollageListQuery({
sortValue: ref('modified_at'),
sortBy: ref('DESC'),
})
</script>CollageListItem Component ​
File: app/components/collage/ListItem.vue
Table-row representation of a collage for list view. Shows the first preview image thumbnail alongside name, description (Vuetify tooltip for truncated text), modified date, created date, and asset count. Lock icon appears when instance_visibility contains no positive values. Options menu (Share, Download, Edit, Portals, Delete) is conditionally shown via permission props.
Props ​
typescript
interface Props {
collage: CollageData // preview_images, assets_count, name, description, instance_visibility
canUpdateCollagePermission?: boolean
backendUrl?: string
modifiedAt?: string
createdAt?: string
downloadColgId?: string | number | null
canShareDownload?: boolean
canEditCollageNameDescription?: boolean
canManageCollagePermission?: boolean
canDeleteCollage?: boolean
}Events ​
typescript
{
openCollage: [event: Event]
shareCollage: []
downloadCollage: []
openRenameDialog: []
openPermissions: []
confirmDeleteCollage: []
toggleOptionsMenu: []
}useCollectionApi Composable ​
File: app/composables/api/useCollectionApi.ts
All collage (collection) API operations. Every method is wrapped with useApiRequestState().track().
Methods ​
typescript
// List all collages (non-paginated)
getCollections(workspace_id: number | string): Promise<CollectionApiResponse>
// Paginated list with sort
getCollectionsByPage(
workspace_id: number | string,
page?: number, // default: 1
sort_value?: string, // default: 'modified_at'
sort_by?: string // default: 'DESC'
): Promise<CollectionApiResponse>
// Recent collages for dashboard widget
getRecentsCollages(workspace_id: number | string): Promise<Collection[]>
// Create a new collage
createCollection(name: string, description?: string | null): Promise<CollectionApiResponse>
// Rename / update description
updateCollectionName(
collectionId: number | string,
workspace_id: number | string,
name: string,
description?: string | null
): Promise<CollectionApiResponse>
// Delete a collage
deleteCollection(collectionId: number | string, workspace_id: number | string): Promise<CollectionApiResponse>
// Generate share token
shareCollection(collectionId: number | string, workspace_id: number | string): Promise<CollectionApiResponse>
// Fetch collage metadata + first page of assets
getCollageDetails(collectionId: number | string, params: CollectionDetailsParams): Promise<CollectionApiResponse>
// Fetch a specific page of assets within a collage
loadCollageAssets(collectionId: number | string, params: CollectionDetailsParams): Promise<CollectionApiResponse>
// Add assets to a collage
addAssetsToCollection(collectionId: number | string, assetsId: number[]): Promise<CollectionApiResponse>
// Remove assets from a collage
removeAssetsFromCollection(
collectionId: number | string,
assetIds: number[],
workspace_id: number | string
): Promise<CollectionApiResponse>useCollageListQuery Composable ​
File: app/composables/queries/useCollageListQuery.ts
TanStack useInfiniteQuery for the collage list. Fetches pages via getCollectionsByPage and augments each preview_image with a unique uuid for stable Vue key binding. hasNextPage is true while allPages.length < lastPage.
Options ​
typescript
interface UseCollageListQueryOptions {
sortValue: Ref<string> // field to sort by, e.g. 'modified_at', 'name'
sortBy: Ref<'ASC' | 'DESC'>
}Returns ​
typescript
{
rawCollages: ComputedRef<Collage[]> // flattened across all loaded pages
isPending: Ref<boolean>
isFetchingNextPage: Ref<boolean>
hasNextPage: Ref<boolean>
error: Ref<unknown>
fetchNextPage: () => void
}useCollageListMutations Composable ​
File: app/composables/queries/useCollageListMutations.ts
TanStack mutations for rename and delete with optimistic patching.
Rename Mutation ​
renameMutation calls updateCollectionName then patches both the infinite-query list cache and the dashboard recents cache directly via patchCollageName — no refetch. Both caches are also marked stale with refetchType: 'none' as a safety net.
Delete Mutation ​
deleteMutation calls deleteCollection then invalidates the list query to trigger a full refetch.
Usage ​
vue
<script setup lang="ts">
const { renameMutation, deleteMutation } = useCollageListMutations()
const onRename = async (payload: { id: number; name: string; description: string }) => {
await renameMutation.mutateAsync({
collectionId: payload.id,
workspaceId: getWorkspaceId(),
name: payload.name,
description: payload.description,
})
}
const onDelete = async (collectionId: number) => {
await deleteMutation.mutateAsync({
collectionId,
workspaceId: getWorkspaceId(),
})
}
</script>Collage Creation Workflow ​
1. User clicks "New Collage" button on the collage list page
↓
2. CreateOrRenameDialog opens
Component: dialogs/CreateOrRenameDialog.vue
- User enters name (required) and optional description
↓
3. Dialog submits
Composable: useCollectionApi.createCollection(name, description)
Endpoint: POST digital-assets/collection/create
↓
4. On success
- useCollageListMutations.invalidateList() triggers refetch
- Router pushes to /:workspace_id/dam/collage/:newId
↓
5. Collage detail page loads
Page: pages/[workspace_id]/dam/collage/[id]/index.vue
- useCollageContentsQuery.fetchDetail() loads metadata
- Empty assets grid shows with "Add assets" promptAsset Management Workflow ​
1. User selects assets in the DAM file browser or search results
Component: DamBulkActionsBar
↓
2. User clicks "Add to Collage" bulk action
Component: dialogs/SaveToCollage.vue
- Lists existing collages (useCollectionApi.getCollections)
- Option to create a new collage inline
↓
3. User picks a collage and confirms
Composable: useCollectionApi.addAssetsToCollection(collectionId, assetIds)
Endpoint: POST digital-assets/collection/:id/add-assets
↓
4. On success
- useCollageContentsQuery.invalidateContents() refreshes the collage
- Success snackbarAPI Integration ​
Collage List ​
Endpoint: GET digital-assets/collection/get-all-by-page
Query Parameters: page, sort_value, sort_by, workspace_id
Response:
json
{
"data": [
{
"id": 12,
"name": "Q4 Campaign",
"description": "Assets for the Q4 push",
"assets_count": 23,
"modified_at": "2026-07-15T10:30:00Z",
"created_at": "2026-06-01T09:00:00Z",
"preview_images": [
{ "url": "https://cdn.example.com/thumb1.jpg", "file_type": "image" },
{ "url": "https://cdn.example.com/thumb2.jpg", "file_type": "image" }
],
"instance_visibility": [1]
}
],
"last_page": 4
}Create Collage ​
Endpoint: POST digital-assets/collection/create
Request:
json
{ "name": "Spring Launch", "description": "Brand assets for spring" }Response:
json
{ "data": { "id": 45, "name": "Spring Launch" }, "message": "Collection created" }Add Assets to Collage ​
Endpoint: POST digital-assets/collection/:id/add-assets
Request:
json
{ "assets_id": [101, 102, 103] }Remove Assets from Collage ​
Endpoint: POST digital-assets/collection/:id/remove-assets
Request:
json
{ "assets_id": [101], "workspace_id": 5 }Component Integration ​
Both collage pages are driven by a single facade composable that owns all state and handlers. Pages destructure everything they need from the facade and pass values directly to components.
Collage List Page (pages/[workspace_id]/dam/collage/index.vue) ​
vue
<template>
<div class="collage-body flex-column">
<!-- "New Collage" button — shown only when canCreateCollagesComputed is true -->
<v-btn v-if="canCreateCollagesComputed" @click="addCollage()">New Collage</v-btn>
<!-- Grid view -->
<Card
v-for="collageItem in collagesList"
:key="collageItem.id"
:collage="collageItem"
:downloading="downloadColgId == collageItem.id"
flag="collage-list"
@edit="openRenameDialog(collageItem)"
@delete="confirmDeleteCollage(collageItem)"
@permission="openPermissions(collageItem)"
@share="shareCollage(collageItem)"
@download="downloadCollage(collageItem)"
/>
<!-- List view -->
<DamLibraryListView
:items="collageItems"
:columns="collageColumns"
:resolve-menu-items="resolveCollageMenu"
:loading-more="loadingMore"
@sort-change="onSortChange"
@item-click="handleItemClick"
@menu-click="handleMenuClick"
@scroll="onScroll"
/>
<ShareAssetDialog :dialog="shareDialog" :files="[collage]" collection @close="shareDialog = false" />
<ConfirmationDialog :dialog="deleteDialog" @confirm="deleteCollage()" @cancel="deleteDialog = false" />
<PermissionDialog :dialog="permissionDialog" :collection-ids="collage.id ? [collage.id] : []" flag="collage" @close="permissionDialog = false" />
<CreateOrRenameDialog :dialog="renameDialog" heading="Update Collage" :file-name="collage.name" is-description @submit="handleChangeCollageName" @close="renameDialog = false" />
</div>
</template>
<script setup lang="ts">
// useCollage is the facade composable for the list page — it wraps
// useCollageListQuery, useCollageListMutations, and all dialog/action state.
const {
loading,
loadingMore,
collagesList,
viewMode,
setViewMode,
onScroll,
collage,
addCollage,
openCollage,
openRenameDialog,
openPermissions,
shareCollage,
downloadCollage,
downloadColgId,
confirmDeleteCollage,
deleteCollage,
changeCollageName,
renameDialog,
shareDialog,
permissionDialog,
deleteDialog,
onPermissionUpdated,
onSortChange,
sortDir,
page_sort_value,
} = useCollage()
// Permission checks + collage list-view menu builder
const { canCreateCollagesComputed, resolveCollageMenu } = useCollageMenu(() => downloadColgId)
</script>Collage Detail Page (pages/[workspace_id]/dam/collage/[id]/index.vue) ​
vue
<template>
<!-- Supports grid, list, and mosaic view modes -->
<AssetCard
v-for="(file, index) in displayFiles"
:key="file.id"
:file="file"
:downloading="file.is_downloading"
@quick-view="openAsset(file)"
@file-select="toggleSelect(file, index, $event)"
@delete="collageCardDelete(file)"
@download="collageCardDownload(file)"
@rename="collageCardRename(file)"
@permission="openPermissionDialog(file, 'asset')"
/>
<DamBulkActionsBar
:selected-count="selectedItemsCount"
:items="bulkActionItems"
@action="onBulkActionClick"
@clear-selection="clearBulkSelection"
/>
<!-- Dialogs -->
<PermissionDialog :dialog="permissionDialog" :flag="flag" @permission-updated="onPermissionUpdated($event, flag)" @close="closePermissionDialog()" />
<CreateOrRenameDialog :dialog="renameDialog" @submit="handleRenameSubmit" @close="closeCreateCollage()" />
<ShareAssetDialog :dialog="shareDialog" @close="closeShareDialog()" />
</template>
<script setup lang="ts">
// useCollageDetails is the facade composable for the detail page — it owns
// asset list state, search/filter state, sort, bulk actions, and all dialogs.
const {
loading,
contentLoading,
assetList,
displayFiles,
viewMode,
changeView,
isPublic,
searchParams,
isSearchActive,
searchLoading,
searchLoadMore,
searchResultsCount,
allFilterList,
handleSearchInput,
clearSearch,
applySearchFilters,
removeFilterOptn,
clearAllFilters,
selectedItemsCount,
selectAll,
showSelectAllBanner,
totalSelectionCount,
confirmSelectAll,
selectAllClass,
toggleSelect,
toggleSelectAll,
openAsset,
deleteDialog,
shareDialog,
renameDialog,
permissionDialog,
flag,
permission_items,
onPermissionUpdated,
closePermissionDialog,
closeShareDialog,
closeCreateCollage,
renameSubmit,
collageCardDelete,
collageCardDownload,
collageCardRename,
openPermissionDialog,
openShareCollage,
openBulkShare,
handleBulkOperation,
loadCollageAssets,
handleScroll,
deleteCollage,
delDialogMsg,
delConfirm,
closeDelDialog,
canUpdateCollagePermission,
canEditCollageNameDescription,
canDeleteCollage,
downldCollage,
canRemoveAssetFromCollage,
isMobileFilterOpen,
toggleMobileFilter,
} = useCollageDetails()
</script>