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. On folder and collage detail pages the card hands preview off to the paging Asset Preview Lightbox.
  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
  • components/dialogs/AssetPreviewDialog.vue — paging lightbox for folder and collage detail pages; arrows, thumbnail strip, swipe, keyboard, "{position} of {total}" counter

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
  • composables/core/dialogs/useAssetPreview.ts, useAssetPreviewPager.ts, useAssetPreviewNavigation.ts, useAssetPreviewGestures.ts — asset preview state, paging, navigation and gestures

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
  • types/asset-preview.ts — AssetPreviewProps, AssetPreviewPagination, AssetPreviewPageResult, AssetPreviewThumbSlot and the option shapes for the preview composables

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
  • With externalPreview, emits expand instead of opening AssetQuickViewDialog (which is then not mounted), and shows the expandGalleyIcon overlay icon
  • 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)
  externalPreview: Boolean   // default false; parent owns the preview (folder/collage detail pages)
}

Events ​

javascript
{
  quickView: [],
  expand: [],               // only with externalPreview
  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>

Asset Preview Lightbox ​

Folder and collage detail pages open assets in AssetPreviewDialog (app/components/dialogs/AssetPreviewDialog.vue, RIV-1224, PR #120) instead of the per-card AssetQuickViewDialog. The dialog pages through every asset in the current listing or search result, not just the cards already rendered, and can also preview a bulk selection. Other pages (search, uploaded, dashboard) still use AssetQuickViewDialog through AssetCard.

How it opens ​

Entry pointCallAssets shownStarts at
Card expand icon (expandGalleyIcon)openFromAsset(asset, () => listing, previewPaging())Live getter over the page's loaded assets, plus pages fetched on demandThe clicked asset
Bulk bar "Preview" (key: 'preview')openFromSelection(selected, undefined, paging?)A snapshot of the selected assets taken at openFirst selected asset

AssetCard is rendered with external-preview. In that mode useAssetCard.handleExpandAsset emits expand instead of opening its own dialog, skips the card's onExpandAssetsCount call, and does not mount AssetQuickViewDialog (v-if="!externalPreview"). The overlay icon switches from lightBoxIcon to expandGalleyIcon through the new expandIcon prop on AssetImageOverlay / AssetVideoOverlay.

The bulk "Preview" item is always the first bulk action and is not affected by the bulk-selection limit. It is built by createBulkActionItem(disabled) with tooltip "Only applicable for assets", and is disabled when:

  • Folder page: the selection contains a folder (selectedItemFolders.length > 0) or no loaded asset is selected.
  • Collage page: no loaded asset is selected.

When "select all" is active (internalSelectAll), the preview pages through the full listing and filters each fetched page to the selection: if the selection is capped (totalSelectionCount > fetchedSelectionIds), every asset except the ones the user deselected counts as selected; otherwise only IDs in selectedItemAssets count. Without "select all", only the loaded selected assets are previewed and there is no paging.

Composables ​

ComposableFileRole
useAssetPreview(options)composables/core/dialogs/useAssetPreview.tsPage-side state: dialog, source ('card' / 'selection'), startAssetId, assets, pagination; openFromAsset, openFromSelection, close, selectedAssetsFrom, createBulkActionItem, isBulkActionItem, trackView
useAssetPreviewPager(options)composables/core/dialogs/useAssetPreviewPager.tsFetches extra pages through pagination.fetchPage(page); keeps a head (pages from the start) and an optional tail (pages from the end)
useAssetPreviewNavigation(options)composables/core/dialogs/useAssetPreviewNavigation.tsCurrent index, position / total, goPrev / goNext / goTo, looping, prefetch, arrow keys
useAssetPreviewGestures(options)composables/core/dialogs/useAssetPreviewGestures.tsStage swipe, thumbnail-strip drag and wheel scrub, neighbour image preloading

Types live in app/types/asset-preview.ts.

Pagination contract ​

A page passes an AssetPreviewPagination object:

ts
interface AssetPreviewPagination {
  fetchPage: (page: number) => Promise<{ items: StoreAsset[]; total?: number; lastPage?: number }>
  loadedPages: number   // pages the grid already holds; the pager starts fetching after these
  pageSize?: number
  total?: number
  lastPage?: number
  prefetchRatio?: number // default 0.6
}
Page / modefetchPage sourcePage sizetotal
Folder, browsinguseFolderContentsQuery().fetchPage with the page's sort and category_id; rows with parent_id (sub-folders) are droppedbackend pagetotal_assets minus loaded sub-folders
Folder, searchingAlready-loaded assetHits slice when available, else runCompatSearch(['digital_assets'], page, 36)COMBINED_PER_PAGE = 36assetsFound
Collage, browsinguseCollageContentsQuery().fetchAssetsPage(id, { sort_value, sort_by, workspace_id, page })backend pagetotalSelectionCount
Collage, searchingLoaded displayFiles slice for pages up to searchCurrentPage, else useCollageDetails().fetchSearchPage(page)40searchResultsCount

fetchSearchPage (new in useCollageDetails) runs performSearch(buildSearchPayload(page)) and maps hits with the same mapSearchHits the grid uses, without touching the grid's own search state. Every fetched item is reset to is_selected: false, is_downloading: false.

Pager rules:

  1. Last page is lastPage, else ceil(total / pageSize), else unbounded. Fetching starts at max(loadedPages, 1).
  2. Items are merged by String(id), so duplicates across pages are dropped. If a fetched page adds nothing new, the pager keeps fetching the next page in the same call.
  3. With an unbounded last page, an empty page marks the head as exhausted.
  4. Going backwards from the first asset (with loop on) fetches the last page into a tail instead of loading every page in between. The gap between head and tail is filled page by page from either side; once the head reaches the tail's first page the two merge.
  5. total is the server total until every page is loaded, then the real item count.
  6. A failed fetch shows "Failed to load assets" (or the API message) and pauses auto-loading until the next manual move. Closing the dialog or changing pagination invalidates in-flight requests through a request token.
  • loop defaults to true. Previous from the first asset goes to the last loaded asset, or requests the last page first if it is not loaded.
  • While a page is loading for a move (pendingMove = forward, backward or last), both arrows are disabled and the relevant arrow shows a spinner. If the load adds nothing usable, the move is retried while more pages exist, then dropped.
  • Prefetch: when the viewed index reaches ceil(headLength * 0.6) the next head page loads in the background; inside the tail, the previous page loads once the user is 60 % of the way back from the tail end.
  • The counter in the header reads "{position} of {total}". Inside the tail, position is counted back from total, so it stays correct while pages in the middle are missing.
  • ArrowLeft / ArrowRight navigate unless a nested dialog (Add to collage, Remove from collage) is open or focus is on an INPUT, TEXTAREA, SELECT, VIDEO, AUDIO or content-editable element.
  • Arrows and the thumbnail strip appear only when total > 1.

Gestures and preloading ​

GestureRule
Stage swipeLeft mouse / primary pointer only, ignored when it starts on button, a, input, textarea, select, video, audio, [role="slider"], .v-slide-group. Horizontal movement must beat vertical; it fires at 50 px, or on a flick of at least 0.5 px/ms and more than 10 px. Swiping left = next
Swipe disabledPDF, document, text and HTML previews (.non-swipeable), and while a nested dialog is open
Thumbnail strip dragStarts after 5 px; each thumbnail width (88 px, 64 px on xs) dragged moves one asset. The click that ends a drag is swallowed
Thumbnail strip wheelHorizontal wheel delta only, one step per thumbnail width
PreloadOn every move, preloads preloadCount (default 2) neighbours on each side with new Image(): compress_file, then display_file, thumbnail_file, preview_image; videos (mp4, mov, avi, mkv, mpg, webm, wmv, m4v) preload only their thumbnail

The strip shows an odd number of slots (at least 3) centred on the current asset. Slots wrap around when looping and total is at least the slot count; otherwise out-of-range slots are empty. A slot whose asset is not loaded yet shows a skeleton and triggers loadNext, loadPrevious or loadLast depending on its side.

View tracking ​

The dialog emits change(asset, index) 400 ms after the current asset settles (CHANGE_SETTLE_MS), so fast paging does not count every asset passed. Pages bind @change="trackView", which for each settled asset:

  1. Dispatches { event: 'asset', sub_event: 'viewed', asset_id: [id], environment: 'admin' } plus collection_id (collage page) or category_id: [id] (folder page) through useTransactionActivity().dispatchTransaction with skipDeduplication: true, so re-viewing within the 5 s dedup window still counts.
  2. Sends the same payload to Amplitude with useAmplitude().trackActivity.
  3. Calls viewAssetsCountApi (POST digital-assets/view-asset-count) with { workspace_id, asset_id }. A failure shows "Failed to track asset view".

Header actions ​

The title row reuses useAssetQuickViewDialog: file name, info tooltip (ID, Credit, Date Uploaded, Last Modified, File Type, and Dimension when width and height exist), Download (hidden with hideDownload), and a menu with Add to collage, Remove From Collage (only on route workspace_id-dam-collage-id) and Open detail view. The menu is hidden when subFlag is FLAG_SHARE_LINK.

  • AssetContainer adds transparent-images to .assets-detail-wrapper for png, svg and gif (useAssetContainer().isTransparentImage), giving them a gray backdrop and padding.
  • AppDialog gains an outside slot rendered after the card (used for the prev/next arrows) and an aria-label="Close" on the close button.
  • useTransactionActivity retries and queue flushes now pass skipDeduplication: true, so a retried or queued event is no longer dropped as a duplicate of its own first attempt.
  • Deleting or moving assets on the folder and collage pages now also removes their IDs from allAssetIds.

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 ​

Version Upload Flow ​

Bulk Delete Flow ​

Cache Refresh After Asset Changes ​

Asset writes outside useAssetMutations refresh the affected TanStack Query caches themselves, so lists do not keep showing old names, thumbnails or counts until staleTime runs out.

TriggerWhereWhat is refreshed
File lands in a folder (mini upload dialog)MiniUploadDialog.vue onUploaded -> useUploadCommon().refreshFolderCounts(parentId)Removes the folder's entry from the localStorage folder-count cache (removeFolderCounts from utils/cacheHelpers.ts, every FOLDERSLISTING_CACHE and FOLDERSLISTING_CACHE:<scope> key), invalidates queryKeys.folders.list(workspaceId) (refetches active Folders lists) and marks every queryKeys.folders.contentsRoot query stale with refetchType: 'none' (refetched on next mount, not immediately).
Asset deleted from the detail pageuseAssetDetail().deleteFileAwaits invalidation of dashboard.recentUploads, dashboard.overview, collections.detailRoot, collections.list and dashboard.recentCollages (useCollageContentsQuery().invalidateListAndRecents), and the parent folder's folders.contents(workspaceId, parentId) before navigating back.
Version uploaded, restored or deleteduseAssetDetail().reloadAssetDataInvalidates and refetches the asset detail, then (not awaited) invalidates the parent folder's contents, dashboard.recentUploads, and marks collections.detailRoot stale with refetchType: 'none'. A restored version therefore shows its own name and thumbnail in the folder listing and collage detail on the next visit.

Rules and edge cases:

  • refreshFolderCounts returns early for a missing or NaN folder id, so uploads without a parent folder skip the folder refresh.
  • It is called for every uploaded file, not only the last one in the batch; the update-category / update-uncategory mitt events are still emitted only for the last file (isLast).
  • Folder contents are invalidated only when the asset has a category_id; root-level assets skip that step.

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"
  }
}