Skip to content

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.

  1. Collage List — pages/[workspace_id]/dam/collage/index.vue renders an infinite-scroll grid or list view of all workspace collages using useCollageListQuery.
  2. Collage Detail — pages/[workspace_id]/dam/collage/[id]/index.vue shows the assets within a single collage with full search and filter support via useCollageDetailsApi.
  3. Collage Card — components/collage/Card.vue renders the mosaic thumbnail with up to three preview images in a 60/40 split layout. Options menu supports rename, share, download, permissions, and delete.
  4. Collage List Item — components/collage/ListItem.vue renders a single collage row in table view with thumbnail, description, dates, asset count, and options.
  5. Creation Flow — dialogs/CreateOrRenameDialog.vue collects name and description; useCollectionApi.createCollection POSTs to the backend.
  6. Mutations — useCollageListMutations wraps 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 dialog
  • app/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 menu
  • app/components/collage/MediaCard.vue — media preview card used within collage detail
  • app/components/collage/MediaControls.vue — playback controls overlay for video assets in collage detail
  • app/components/collage/VideoControls.vue — video-specific playback controls
  • app/components/collage/PreviewModal.vue — full-screen asset preview within the collage context
  • app/components/collage/FolderCard.vue — folder card variant for use inside collage detail
  • app/components/collage/RecentFolderListItem.vue — recent-folder list row used in the dashboard recents widget
  • app/components/collage/RecentUploadListItem.vue — recent-upload list row used in the dashboard recents widget
  • app/components/dialogs/CreateOrRenameDialog.vue — shared dialog for creating a new collage or renaming an existing one; supports optional description field
  • app/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 assets
  • app/composables/api/useCollageDetailsApi.ts — detail-page composable combining useSearchApi.getSearchResults and useCommonApis.getInnerSearchData for the in-collage search and filter pool
  • app/composables/queries/useCollageListQuery.ts — TanStack infinite query for the collage list; handles pagination, sorting, and preview-image uuid augmentation
  • app/composables/queries/useCollageContentsQuery.ts — TanStack fetch-query wrapper for collage detail metadata and paginated asset pages; exposes invalidateContents for post-mutation invalidation
  • app/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 scroll
  • app/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, CollectionApiResponse
  • app/types/dashboard.ts — Collage (includes preview_images, assets_count, name, description, instance_visibility)

Page Details ​

Collage List ([workspace_id]/dam/collage/index.vue) ​

FieldValue
Route/:workspace_id/dam/collage
Layoutcollage-layout
Middlewareauth-check, check-workspace, check-if-suspended, can-access-dam-module
Primary composablesuseCollage(), 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) ​

FieldValue
Route/:workspace_id/dam/collage/:id
Layoutcollage-layout (embedsHeader: true)
Middlewareauth-check, check-workspace, check-if-suspended, can-access-dam-module
Primary composablesuseCollageDetails(), 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 CreateOrRenameDialog with name + description fields
  • Options menu (Share, Download, Edit, Portals, Delete) — items conditionally visible per permission props
  • Lock icon tooltip when instance_visibility indicates restricted access
  • Delegates behavior to useCollageCard composable 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" prompt

Asset 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 snackbar

API 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>