Skip to content

Asset Management ​

Overview ​

Assets are the core entity in Collage Admin. The asset management system covers the full lifecycle from upload through preview, editing, versioning, bulk operations, and deletion.

  1. Upload — a two-step chunked upload flow: the Nitro server signs an S3 multipart request, the client uploads directly to S3, then registers the asset with the Laravel API. Upload state is managed in a batched reactive queue capped at 50 concurrent items.
  2. Display — AssetCard renders each asset as a grid tile with hover overlays, context menus, video autoplay preview, and a quick-view lightbox. It delegates all business logic to the useAssetCard composable.
  3. Bulk Actions — DamBulkActionsBar appears when one or more assets are selected. It is a draggable floating toolbar providing download, move, copy, tag, delete, collage, and visibility actions.
  4. Image Editing — ImageEditor wraps Cropper.js and supports crop, rotate, flip, and resize. Each edit produces a new asset version rather than overwriting the original.
  5. List View — UploadedListItem renders an asset as a table row in the uploaded assets page, with inline rename, custom fields display, tags display, and a context action menu.
  6. Mutations — useAssetMutations provides TanStack mutations for field updates and soft-deletes, invalidating both the recent-uploads dashboard cache and the trash cache on success.

Architecture ​

Upload is orchestrated entirely in AssetUpload.vue. Files are gathered from drag-and-drop or browser file picker, normalized into UploadFileItem objects, and placed in a non-reactive master list (masterFileLister). A batching function (processNextBatch) drains up to 50 items at a time into the reactive queue for rendering via UploadChunk. Each UploadChunk handles its own S3 multipart upload and fires uploaded when complete; AssetUpload then calls the Nitro signed-URL endpoint (/api/s3/get-signed-url), followed by the Laravel digital-assets/upload endpoint to register the asset. For version uploads, the endpoint switches to digital-assets/version/upload. For external guest uploads, it uses external-upload. After registration the composable emits uploaded and triggers Amplitude analytics tracking via useAmplitude. Asset queries are managed by TanStack Query via useAssetsApi and useAssetMutations; mutations invalidate the dashboard recent-uploads and overview query keys so the dashboard updates without a manual refresh.

File Structure ​

Vue Component Files ​

  • components/asset/AssetCard.vue — grid card for a single asset; handles video hover-play, image lazy loading, selection, context menu, quick-view dialog, share dialog, save-to-collage dialog
  • components/asset/AssetUpload.vue — drag-and-drop upload zone supporting files, folders, versioning, and external guest uploads; orchestrates the batched S3 multipart upload flow
  • components/asset/AssetImageOverlay.vue — image card overlay with selection checkbox, menu trigger, and quick-view/download shortcut buttons
  • components/asset/AssetVideoOverlay.vue — video card overlay with the same controls as AssetImageOverlay, tuned for video thumbnails
  • components/asset/ImageEditor.vue — Cropper.js-based image editing canvas; crop, rotate, flip, resize with full action history
  • components/asset/ImageEditActions.vue — toolbar above the image editor with action buttons (crop, rotate CW/CCW, flip H/V, reset, download)
  • components/asset/UploadedListItem.vue — table row for a single asset in the uploaded-assets list; inline rename, tags chips, custom field chips, collage queue chips, context menu
  • components/asset/AssetVersionItem.vue — single row in the asset version history panel
  • components/asset/UploadChunk.vue — handles S3 multipart upload for one file item; emits progress, uploaded, error, retry events
  • components/asset/UploadChunkExternal.vue — same as UploadChunk but for external guest upload flow
  • components/asset/UploadBackdrop.vue — modal backdrop used when the upload panel is shown in overlay mode
  • components/asset/MiniUploadDialog.vue — compact upload dialog accessible from the DAM header
  • components/asset/AssetContainer.vue — container component that wraps asset grid or list view with shared state
  • components/asset/AddTags.vue — inline tag input for adding tags to an asset
  • components/asset/AssetCustomFieldsSection.vue — renders all custom fields for an asset in the detail panel
  • components/asset/MediaResources.vue — injects media preload hints for the current asset's thumbnail and video preview
  • components/dam/DamBulkActionsBar.vue — draggable floating toolbar shown when one or more assets are selected

Composable Files ​

  • composables/api/useAssetsApi.ts — all asset API calls: getRecentUploaded, updateWithField, deleteFile, getAssetDetails, archiveAsset, convertAsset, convertResolution, recordDownloadHistory, deleteVersion, makeCurrentVersion
  • composables/api/useAssetDetailApi.ts — detail-level queries for a single asset (metadata, sharing, analytics)
  • composables/queries/useAssetMutations.ts — TanStack mutations for updateWithField and deleteFile; invalidates dashboard and trash caches on success
  • composables/queries/useFolderContentsQuery.ts — per-folder asset list with TanStack cache; used when browsing assets inside a folder

Server API Files ​

  • server/api/s3/multipart/create.ts — signs an S3 multipart upload initiation request server-side
  • server/api/s3/multipart/complete.ts — completes a multipart upload after all parts are uploaded
  • server/api/s3/get-signed-url.ts — returns a signed URL for an uploaded asset after the S3 upload is done
  • server/api/s3/delete-asset.ts — removes a cancelled or failed upload from S3

Type Files ​

  • types/asset.ts — AssetParams, AssetApiResponse, UpdateAssetPayload, DeleteFilePayload, AssetDetailsPayload, ConvertPayload, DownloadHistoryPayload, MakeVersionPayload
  • types/upload.ts — UploadFileItem, UploadFileInfo, UploadCompleteEvent, DamUploadResponse
  • types/store.ts — StoreAsset — the canonical asset type used by AssetCard and the DAM store

Page Files ​

  • app/pages/[workspace_id]/dam/upload.vue — upload entry point; thin wrapper that renders <AssetUpload mode="both" /> and applies auth + workspace middleware
  • app/pages/[workspace_id]/dam/uploaded.vue — uploaded assets review page; two tabs (Uploaded / Requested), list-row layout via UploadedListItem, deferred collage queue, marquee selection, bulk actions
  • app/pages/[workspace_id]/dam/files/[_id]/index.vue — asset detail page; split-pane viewer with a 7-tab sidebar panel

Page Details ​

Upload ([workspace_id]/dam/upload.vue) ​

FieldValue
Route/:workspace_id/dam/upload
Layoutcollage-layout
Middlewareauth-check, check-workspace, check-workspace-access, can-access-dam-module, check-if-suspended
Primary composablenone — page delegates entirely to <AssetUpload mode="both" />

This page is an intentionally thin wrapper. All upload logic (file picking, drag-and-drop, S3 multipart, progress tracking) lives in the AssetUpload component.


Uploaded Assets ([workspace_id]/dam/uploaded.vue) ​

FieldValue
Route/:workspace_id/dam/uploaded
Layoutcollage-layout
Middlewareauth-check, check-workspace, check-workspace-access, can-access-dam-module, check-if-suspended
Primary composableuseUploadedPage()

Tabs

TabContent
UploadedList of recently uploaded assets (UploadedListItem rows) with marquee selection
RequestedAssets uploaded by external contributors via portal request links

Bulk actions bar (appears when items are selected)

ActionPermission gate
Add to CollagecanManageCollages
Add TagscanManageTags
Custom FieldscanManageCustomFields
MovecanMoveAssets
DeletecanDeleteAssets

Dialogs: ConfirmationDialog, AddMultipleTags, SaveToCollage, ManageCustomFields, FolderDialog


Asset Detail ([workspace_id]/dam/files/[_id]/index.vue) ​

FieldValue
Route/:workspace_id/dam/files/:_id
Layoutcollage-layout
Middlewareauth-check, check-workspace, check-workspace-access, can-access-dam-module, check-if-suspended
Primary composableuseAssetDetail()

The page renders in a split-pane layout: the left pane shows the asset viewer (image, video, audio, document, or generic file icon) and the right pane shows a tabbed detail panel.

Sidebar tabs

TabContentSubscription gate
Asset InfoMetadata, file info, download buttonNo
TagsTag list and tag editorNo
Custom FieldsCustom field valuesNo
InsightsPer-asset view analyticsYes — Pro plan
CollageCollages this asset belongs toNo
Version HistoryVersion list with restore and promote actionsYes — Pro plan
Image EditorIn-browser crop, resize, and filter editorNo

Dialogs: ShareAssetDialog, ConfirmationDialog, SaveToCollage, FolderDialog, ManageCustomFields, FileConversionDialog


AssetCard ​

The primary asset display component. Switches between video (with hover-play <video>) and image (<v-img>) layouts based on the asset's file_type. Delegates all state and logic to the useAssetCard composable.

Features ​

  • Video hover-play using mouseenter/mouseleave on the card
  • Lazy image loading with fetchpriority="high" on first-visible images (isFirstImage prop)
  • Folder-thumbnail rendering when file.parent_id is set
  • Integrated ShareAssetDialog, SaveToCollage, and AssetQuickViewDialog modals
  • Lock icon overlay for restricted-visibility assets (skipped when flag === 'trash-listing')
  • Selection state via isSelected from useAssetCard

Props ​

javascript
{
  mosaic: Boolean,           // compact mosaic mode (no footer, no overlays)
  file: StoreAsset,          // the asset to display (required)
  insideFolder: Boolean,     // asset is being displayed inside a folder view
  downloading: Boolean,      // download in progress; shows spinner
  searchPage: Boolean,       // asset is on the search results page
  flag: String,              // contextual modifier: '' | 'trash-listing' | 'external'
  shiftKey: Number,          // shift-key state for range selection
  isFirstImage: Boolean,     // hints the browser to eager-load this asset
  hideDownload: Boolean,     // suppress the download button in overlays
  canRestore: Boolean        // show restore action (trash context)
}

Events ​

javascript
{
  quickView: [],
  load: [],
  assetView: [],
  permissionChanged: [],
  fileSelect: [],
  delete: [],
  restore: [],
  rename: [],
  permission: [],
  move: [],
  duplicate: [],
  download: [file: Record<string, unknown>]
}

Usage Example ​

vue
<script setup lang="ts">
import type { StoreAsset } from '~/types/store'

const props = defineProps<{ assets: StoreAsset[] }>()
</script>

<template>
  <div class="asset-grid">
    <AssetCard
      v-for="(asset, index) in assets"
      :key="asset.id"
      :file="asset"
      :is-first-image="index < 6"
      flag=""
      @delete="handleDelete(asset)"
      @restore="handleRestore(asset)"
    />
  </div>
</template>

AssetUpload ​

Drag-and-drop upload zone. Handles standard file uploads, folder uploads (recursive structure preserved), asset version replacement, and the external guest upload flow. Uses a batching strategy to keep the reactive queue under 50 items while a non-reactive master list holds all pending files.

Performance Constants ​

ConstantValuePurpose
MAX_FILES_PER_BATCH50Files processed per drop batch
LARGE_UPLOAD_THRESHOLD500Above this count, thumbnails use icon fallbacks
ACTIVE_QUEUE_SIZE50Max concurrent items in the reactive queue
INTERNAL_LIMIT_STR'5GB'Per-file size cap for admin uploads

Props ​

javascript
{
  file: { id?: number } | null,  // existing asset for version upload
  isVersion: Boolean,            // true = version-upload mode
  mode: String,                  // 'both' | 'file' | 'folder'
  flag: String                   // '' | 'external'
}

Events ​

javascript
{
  uploaded: [data: unknown],          // fired per successful upload
  'version-uploaded': [data: unknown],// fired on successful version upload
  close: []                           // user closed the upload panel
}

Usage Example ​

vue
<script setup lang="ts">
function onAssetUploaded(data: unknown) {
  // Refresh folder contents or dashboard after upload
}
</script>

<template>
  <!-- Standard multi-file upload in a folder context -->
  <AssetUpload
    mode="both"
    flag=""
    @uploaded="onAssetUploaded"
    @close="uploadPanelOpen = false"
  />

  <!-- Version upload for an existing asset -->
  <AssetUpload
    :file="{ id: existingAsset.id }"
    :is-version="true"
    mode="file"
    @version-uploaded="onVersionUploaded"
    @close="versionDialogOpen = false"
  />
</template>

DamBulkActionsBar ​

Floating draggable toolbar shown when selectedCount > 0. Inline icons for the most common actions overflow into a v-menu on smaller viewports. Drag position is managed by the useDraggableBar composable.

Props ​

javascript
{
  selectedCount: Number,          // number of selected items (required)
  items: DamMenuItem[],           // action definitions (required)
  contentLoading: Boolean,        // show skeleton state
  skeletonCount: Number,          // number of skeleton chips to show
  floating: Boolean,              // enable draggable floating mode
  showSelectAllPrompt: Boolean,   // show "select all N" prompt
  totalSelectionCount: Number,    // total count for the select-all prompt
  selectAllLoading: Boolean,      // disable select-all while loading
  maxVisibleIcons: Number         // cap on inline icons before overflow
}

Events ​

javascript
{
  'clear-selection': [],          // user clicked the count chip to deselect all
  'select-all': [],               // user clicked the select-all prompt
  action: [item: DamMenuItem]     // user triggered a bulk action
}

Usage Example ​

vue
<script setup lang="ts">
import type { DamMenuItem } from '~/types/dam-list'

const selectedCount = ref(0)
const bulkActions: DamMenuItem[] = [
  { key: 'download', label: 'Download', icon: 'downloadIcon' },
  { key: 'move', label: 'Move', icon: 'moveIcon' },
  { key: 'delete', label: 'Delete', icon: 'deleteIcon' },
]

function onBulkAction(item: DamMenuItem) {
  if (item.key === 'delete') handleBulkDelete()
  if (item.key === 'move') handleBulkMove()
}
</script>

<template>
  <DamBulkActionsBar
    :selected-count="selectedCount"
    :items="bulkActions"
    :floating="true"
    @clear-selection="selectedCount = 0"
    @action="onBulkAction"
  />
</template>

ImageEditor ​

Cropper.js canvas for non-destructive image editing. Each exported edit is uploaded as a new asset version. Action history allows undo-chain replay. The editor reports ready/error state via events, enabling parent components to show their own loading overlays.

Props ​

javascript
{
  src: String,                          // image URL to load in the editor (required)
  alt: String,                          // img alt text
  mimeType: String,                     // MIME type for export (default: 'image/png')
  isCompress: Boolean,                  // enable compression on export
  originalData: Object | null,          // original width/height/mimeType for reset
  originalImageData: ArrayBuffer | null,// raw bytes for lossless operations
  imageDataLoading: Boolean             // parent loading state for the image bytes
}

Events ​

javascript
{
  'cropper-ready': [ready: Boolean],
  'image-error': [value: Boolean],
  'update:dimensions': [dims: ImageDimensionsUpdate],
  'update:transform': [state: TransformState],
  'export-error': [message: String],
  'img-editor-processing': [processing: Boolean],
  'cropper-img-loaded': [loading: Boolean],
  'img-editor-cropbox-data': [data: CropBoxData],
  'is-active-crop': [],
  'img-edit-download': []
}

Usage Example ​

vue
<script setup lang="ts">
const imageSrc = ref('https://cdn.example.com/asset.jpg')
const isReady = ref(false)

function onCropperReady(ready: boolean) {
  isReady.value = ready
}
</script>

<template>
  <div class="editor-wrapper">
    <ImageEditor
      :src="imageSrc"
      mime-type="image/jpeg"
      :is-compress="true"
      @cropper-ready="onCropperReady"
      @img-editor-processing="showSpinner = $event"
      @img-edit-download="onDownload"
    />
    <ImageEditActions :disabled="!isReady" />
  </div>
</template>

useAssetsApi ​

API composable for asset operations. All methods are wrapped in track() from useApiRequestState.

Methods ​

javascript
{
  getRecentUploaded: (params: AssetParams) => Promise<any>,
  updateWithField: (payload: UpdateAssetPayload) => Promise<AssetApiResponse>,
  deleteFile: (payload: DeleteFilePayload) => Promise<AssetApiResponse>,
  viewAssetsCountApi: (payload: AssetDetailsPayload) => Promise<AssetApiResponse>,
  getAssetDetails: (payload: AssetDetailsPayload) => Promise<AssetApiResponse>,
  getAssetsAnalyticsSummary: (payload: AnalyticsPayload) => Promise<AssetApiResponse>,
  archiveAsset: (payload: UpdateAssetPayload) => Promise<AssetApiResponse>,
  convertAsset: (payload: ConvertPayload) => Promise<AssetApiResponse>,
  convertResolution: (payload: ConvertPayload) => Promise<Blob>,
  recordDownloadHistory: (payload: DownloadHistoryPayload) => Promise<AssetApiResponse>,
  deleteVersion: (selectedVersion: number | string, assetId: number | string, activeVersion: number | string) => Promise<AssetApiResponse>,
  makeCurrentVersion: (payload: MakeVersionPayload) => Promise<AssetApiResponse>,
  isLoading: Ref<boolean>,
  error: Ref<Error | null>
}

useAssetMutations ​

TanStack mutations for asset writes in the uncategorized/recent-uploads page context. Folder and collage pages manage their own invalidation through scoped composables (useFolderContentsQuery, useCollageContentsQuery).

Mutations ​

javascript
{
  updateAssetFieldMutation: UseMutationReturnType<AssetApiResponse, UpdateAssetPayload>,
  deleteAssetMutation: UseMutationReturnType<AssetApiResponse, DeleteFilePayload>
}

On updateAssetFieldMutation success: invalidates queryKeys.dashboard.recentUploads and queryKeys.dashboard.overview.

On deleteAssetMutation success: same plus queryKeys.trash.all (soft-deleted assets appear in trash).

Workflows ​

S3 Multipart Upload Flow ​

User drops file on AssetUpload drop zone
  → onFileDrop() validates file size (5 GB internal / configurable external limit)
  → File added to masterFileLister[]
  → processNextBatch() drains up to 50 items into reactive queue[]
  → UploadChunk renders for each queue item
    → UploadChunk calls POST /api/s3/multipart/create (Nitro signs request)
    → UploadChunk uploads parts directly to S3
    → UploadChunk calls POST /api/s3/multipart/complete
    → UploadChunk emits 'uploaded' with response
  → AssetUpload.onChunkUploaded():
    → GET /api/s3/get-signed-url (Nitro returns CDN URL)
    → POST digital-assets/upload (registers asset in Laravel)
    → emit('uploaded', data)
    → damStore.updateStorage() (refresh storage quota)
    → Amplitude trackActivity()
    → processNextBatch() (drain next batch from masterFileLister)

Version Upload Flow ​

User opens version upload dialog for an existing asset
  → AssetUpload mounted with :is-version="true" and :file="{ id: assetId }"
  → User selects a single file
  → Same S3 multipart flow as above
  → On success, calls POST digital-assets/version/upload instead of digital-assets/upload
  → emit('version-uploaded', data)
  → Dialog closes automatically

Bulk Delete Flow ​

User selects assets → selects Delete from DamBulkActionsBar
  → emit('action', { key: 'delete' })
  → Parent calls deleteAssetMutation.mutate({ payload })
  → POST digital-assets/delete-file
  → onSuccess: invalidate recentUploads + overview + trash caches
  → Assets removed from list view
  → Assets appear in trash page

API Integration ​

Endpoints ​

EndpointMethodDescription
/api/s3/multipart/createPOSTServer-side signs S3 multipart initiation
/api/s3/multipart/completePOSTCompletes the multipart upload
/api/s3/get-signed-urlGETReturns CDN-signed URL after upload
/api/s3/delete-assetDELETERemoves a failed/cancelled upload from S3
digital-assets/uploadPOSTRegisters an uploaded asset in the Laravel backend
digital-assets/version/uploadPOSTRegisters a new version for an existing asset
external-uploadPOSTRegisters an asset uploaded by a guest user
digital-assets/delete-filePOSTSoft-deletes an asset (moves to trash)
digital-assets/update-with-fieldPOSTUpdates a single field on an asset
digital-assets/version/make-current-versionPOSTPromotes a version to current
digital-assets/version/:id/removeDELETEHard-deletes a specific version

Upload Registration Request ​

json
{
  "workspace_id": "123",
  "file_name": "uuid-from-s3",
  "display_file_name": "my-photo.jpg",
  "display_file": "https://cdn.example.com/signed-url",
  "auto_tag": 1,
  "mime_type": "image/jpeg",
  "file_extension": "jpg",
  "file_size": 2048576,
  "category_id": 45
}

Upload Registration Response ​

json
{
  "data": {
    "id": 9821,
    "category_id": 45,
    "display_file_name": "my-photo.jpg",
    "file_type": "jpg",
    "file_size": 2048576,
    "thumbnail_file": "https://cdn.example.com/thumb/my-photo.jpg"
  }
}