Appearance
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.
- 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.
- Display —
AssetCardrenders 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 theuseAssetCardcomposable. On folder and collage detail pages the card hands preview off to the paging Asset Preview Lightbox. - Bulk Actions —
DamBulkActionsBarappears when one or more assets are selected. It is a draggable floating toolbar providing download, move, copy, tag, delete, collage, and visibility actions. - Image Editing —
ImageEditorwraps Cropper.js and supports crop, rotate, flip, and resize. Each edit produces a new asset version rather than overwriting the original. - List View —
UploadedListItemrenders an asset as a table row in the uploaded assets page, with inline rename, custom fields display, tags display, and a context action menu. - Mutations —
useAssetMutationsprovides 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 dialogcomponents/asset/AssetUpload.vue— drag-and-drop upload zone supporting files, folders, versioning, and external guest uploads; orchestrates the batched S3 multipart upload flowcomponents/asset/AssetImageOverlay.vue— image card overlay with selection checkbox, menu trigger, and quick-view/download shortcut buttonscomponents/asset/AssetVideoOverlay.vue— video card overlay with the same controls asAssetImageOverlay, tuned for video thumbnailscomponents/asset/ImageEditor.vue— Cropper.js-based image editing canvas; crop, rotate, flip, resize with full action historycomponents/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 menucomponents/asset/AssetVersionItem.vue— single row in the asset version history panelcomponents/asset/UploadChunk.vue— handles S3 multipart upload for one file item; emits progress, uploaded, error, retry eventscomponents/asset/UploadChunkExternal.vue— same asUploadChunkbut for external guest upload flowcomponents/asset/UploadBackdrop.vue— modal backdrop used when the upload panel is shown in overlay modecomponents/asset/MiniUploadDialog.vue— compact upload dialog accessible from the DAM headercomponents/asset/AssetContainer.vue— container component that wraps asset grid or list view with shared statecomponents/asset/AddTags.vue— inline tag input for adding tags to an assetcomponents/asset/AssetCustomFieldsSection.vue— renders all custom fields for an asset in the detail panelcomponents/asset/MediaResources.vue— injects media preload hints for the current asset's thumbnail and video previewcomponents/dam/DamBulkActionsBar.vue— draggable floating toolbar shown when one or more assets are selectedcomponents/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,makeCurrentVersioncomposables/api/useAssetDetailApi.ts— detail-level queries for a single asset (metadata, sharing, analytics)composables/queries/useAssetMutations.ts— TanStack mutations forupdateWithFieldanddeleteFile; invalidates dashboard and trash caches on successcomposables/queries/useFolderContentsQuery.ts— per-folder asset list with TanStack cache; used when browsing assets inside a foldercomposables/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-sideserver/api/s3/multipart/complete.ts— completes a multipart upload after all parts are uploadedserver/api/s3/get-signed-url.ts— returns a signed URL for an uploaded asset after the S3 upload is doneserver/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,MakeVersionPayloadtypes/upload.ts—UploadFileItem,UploadFileInfo,UploadCompleteEvent,DamUploadResponsetypes/store.ts—StoreAsset— the canonical asset type used byAssetCardand the DAM storetypes/asset-preview.ts—AssetPreviewProps,AssetPreviewPagination,AssetPreviewPageResult,AssetPreviewThumbSlotand 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 middlewareapp/pages/[workspace_id]/dam/uploaded.vue— uploaded assets review page; two tabs (Uploaded / Requested), list-row layout viaUploadedListItem, deferred collage queue, marquee selection, bulk actionsapp/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) ​
| Field | Value |
|---|---|
| Route | /:workspace_id/dam/upload |
| Layout | collage-layout |
| Middleware | auth-check, check-workspace, check-workspace-access, can-access-dam-module, check-if-suspended |
| Primary composable | none — 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) ​
| Field | Value |
|---|---|
| Route | /:workspace_id/dam/uploaded |
| Layout | collage-layout |
| Middleware | auth-check, check-workspace, check-workspace-access, can-access-dam-module, check-if-suspended |
| Primary composable | useUploadedPage() |
Tabs
| Tab | Content |
|---|---|
| Uploaded | List of recently uploaded assets (UploadedListItem rows) with marquee selection |
| Requested | Assets uploaded by external contributors via portal request links |
Bulk actions bar (appears when items are selected)
| Action | Permission gate |
|---|---|
| Add to Collage | canManageCollages |
| Add Tags | canManageTags |
| Custom Fields | canManageCustomFields |
| Move | canMoveAssets |
| Delete | canDeleteAssets |
Dialogs: ConfirmationDialog, AddMultipleTags, SaveToCollage, ManageCustomFields, FolderDialog
Asset Detail ([workspace_id]/dam/files/[_id]/index.vue) ​
| Field | Value |
|---|---|
| Route | /:workspace_id/dam/files/:_id |
| Layout | collage-layout |
| Middleware | auth-check, check-workspace, check-workspace-access, can-access-dam-module, check-if-suspended |
| Primary composable | useAssetDetail() |
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
| Tab | Content | Subscription gate |
|---|---|---|
| Asset Info | Metadata, file info, download button | No |
| Tags | Tag list and tag editor | No |
| Custom Fields | Custom field values | No |
| Insights | Per-asset view analytics | Yes — Pro plan |
| Collage | Collages this asset belongs to | No |
| Version History | Version list with restore and promote actions | Yes — Pro plan |
| Image Editor | In-browser crop, resize, and filter editor | No |
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/mouseleaveon the card - Lazy image loading with
fetchpriority="high"on first-visible images (isFirstImageprop) - Folder-thumbnail rendering when
file.parent_idis set - Integrated
ShareAssetDialog,SaveToCollage, andAssetQuickViewDialogmodals - With
externalPreview, emitsexpandinstead of openingAssetQuickViewDialog(which is then not mounted), and shows theexpandGalleyIconoverlay icon - Lock icon overlay for restricted-visibility assets (skipped when
flag === 'trash-listing') - Selection state via
isSelectedfromuseAssetCard
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 ​
| Constant | Value | Purpose |
|---|---|---|
MAX_FILES_PER_BATCH | 50 | Files processed per drop batch |
LARGE_UPLOAD_THRESHOLD | 500 | Above this count, thumbnails use icon fallbacks |
ACTIVE_QUEUE_SIZE | 50 | Max 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 point | Call | Assets shown | Starts at |
|---|---|---|---|
Card expand icon (expandGalleyIcon) | openFromAsset(asset, () => listing, previewPaging()) | Live getter over the page's loaded assets, plus pages fetched on demand | The clicked asset |
Bulk bar "Preview" (key: 'preview') | openFromSelection(selected, undefined, paging?) | A snapshot of the selected assets taken at open | First 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 ​
| Composable | File | Role |
|---|---|---|
useAssetPreview(options) | composables/core/dialogs/useAssetPreview.ts | Page-side state: dialog, source ('card' / 'selection'), startAssetId, assets, pagination; openFromAsset, openFromSelection, close, selectedAssetsFrom, createBulkActionItem, isBulkActionItem, trackView |
useAssetPreviewPager(options) | composables/core/dialogs/useAssetPreviewPager.ts | Fetches 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.ts | Current index, position / total, goPrev / goNext / goTo, looping, prefetch, arrow keys |
useAssetPreviewGestures(options) | composables/core/dialogs/useAssetPreviewGestures.ts | Stage 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 / mode | fetchPage source | Page size | total |
|---|---|---|---|
| Folder, browsing | useFolderContentsQuery().fetchPage with the page's sort and category_id; rows with parent_id (sub-folders) are dropped | backend page | total_assets minus loaded sub-folders |
| Folder, searching | Already-loaded assetHits slice when available, else runCompatSearch(['digital_assets'], page, 36) | COMBINED_PER_PAGE = 36 | assetsFound |
| Collage, browsing | useCollageContentsQuery().fetchAssetsPage(id, { sort_value, sort_by, workspace_id, page }) | backend page | totalSelectionCount |
| Collage, searching | Loaded displayFiles slice for pages up to searchCurrentPage, else useCollageDetails().fetchSearchPage(page) | 40 | searchResultsCount |
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:
- Last page is
lastPage, elseceil(total / pageSize), else unbounded. Fetching starts atmax(loadedPages, 1). - 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. - With an unbounded last page, an empty page marks the head as exhausted.
- 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.
totalis the server total until every page is loaded, then the real item count.- 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
paginationinvalidates in-flight requests through a request token.
Navigation rules ​
loopdefaults totrue. 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,backwardorlast), 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,positionis counted back fromtotal, so it stays correct while pages in the middle are missing. ArrowLeft/ArrowRightnavigate unless a nested dialog (Add to collage, Remove from collage) is open or focus is on anINPUT,TEXTAREA,SELECT,VIDEO,AUDIOor content-editable element.- Arrows and the thumbnail strip appear only when
total > 1.
Gestures and preloading ​
| Gesture | Rule |
|---|---|
| Stage swipe | Left 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 disabled | PDF, document, text and HTML previews (.non-swipeable), and while a nested dialog is open |
| Thumbnail strip drag | Starts 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 wheel | Horizontal wheel delta only, one step per thumbnail width |
| Preload | On 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:
- Dispatches
{ event: 'asset', sub_event: 'viewed', asset_id: [id], environment: 'admin' }pluscollection_id(collage page) orcategory_id: [id](folder page) throughuseTransactionActivity().dispatchTransactionwithskipDeduplication: true, so re-viewing within the 5 s dedup window still counts. - Sends the same payload to Amplitude with
useAmplitude().trackActivity. - 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.
Related changes in PR #120 ​
AssetContaineraddstransparent-imagesto.assets-detail-wrapperforpng,svgandgif(useAssetContainer().isTransparentImage), giving them a gray backdrop and padding.AppDialoggains anoutsideslot rendered after the card (used for the prev/next arrows) and anaria-label="Close"on the close button.useTransactionActivityretries and queue flushes now passskipDeduplication: 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.
| Trigger | Where | What 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 page | useAssetDetail().deleteFile | Awaits 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 deleted | useAssetDetail().reloadAssetData | Invalidates 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:
refreshFolderCountsreturns early for a missing orNaNfolder 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-uncategorymitt 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 ​
| Endpoint | Method | Description |
|---|---|---|
/api/s3/multipart/create | POST | Server-side signs S3 multipart initiation |
/api/s3/multipart/complete | POST | Completes the multipart upload |
/api/s3/get-signed-url | GET | Returns CDN-signed URL after upload |
/api/s3/delete-asset | DELETE | Removes a failed/cancelled upload from S3 |
digital-assets/upload | POST | Registers an uploaded asset in the Laravel backend |
digital-assets/version/upload | POST | Registers a new version for an existing asset |
external-upload | POST | Registers an asset uploaded by a guest user |
digital-assets/delete-file | POST | Soft-deletes an asset (moves to trash) |
digital-assets/update-with-field | POST | Updates a single field on an asset |
digital-assets/version/make-current-version | POST | Promotes a version to current |
digital-assets/version/:id/remove | DELETE | Hard-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"
}
}