Appearance
Trash ​
Overview ​
Soft-delete holding area: Assets and folders moved to trash are not immediately removed from the database. They are flagged as deleted and held for 30 days before automatic permanent removal. This gives users a safety window to recover mistakenly deleted content.
Role-based access: Two permission tiers govern the trash page. Managers and admins can restore items. Admins only can permanently delete items or empty the entire trash. Viewers have no access to trash actions.
Mixed content listing: The page fetches both deleted folders and deleted assets in a single paginated API call. Folders are listed before files in both grid and list views.
Dual view modes: Items can be viewed in a list layout (with sortable columns for Name, Date Deleted, Date Added, File Size, File Type) or a grid layout (card thumbnails). The selected mode persists within the session.
Bulk and single-item operations: Users can select individual items via checkbox, use shift-click range selection for bulk operations, or use the "Restore All" and "Empty Trash" options from the page-level options menu.
Infinite scroll pagination: The page loads 36 items per page and fetches additional pages when the user scrolls within 85% of the container's scroll height.
Architecture ​
The trash page is a standalone Nuxt page (pages/_workspace_id/dam/trash.vue) that uses the damLayout layout and the same five middleware guards as other DAM pages.
All operations are handled directly in the page component — no Vuex store actions are called. The component makes three API calls: one to list deleted items (paginated), one to permanently delete, and one to restore. Both delete and restore operations update local state immediately after the API call succeeds rather than re-fetching, keeping the list responsive.
Shift-click multi-select is provided by the shiftSelection mixin, which tracks lastSelectedIndex across the combined folder + file array. The mixin's toggleSelectTrash method handles the range calculation and updates the is_selected flag on each item object.
The confirmation dialog is shared with the rest of the DAM module (ConfirmationDialog.vue). A set of computed properties — delDialogHeader, delDialogMsg, and getDelDialog — derive the dialog heading and body text from the current operation flags (empty_trash, restore_all, delete_selected, restore_selected) and the selected item counts.
Grid view delegates rendering to FileListingCard.vue (assets) and FolderCard.vue (folders), each receiving a flag="trash-listing" prop that enables the Restore context menu option in addition to Delete. List view renders directly in the page template with v-list-item rows and an inline options menu per row.
Amplitude events (trash permanently-deleted, trash restored) are fired after each operation with asset/folder ID arrays, counts, and selection method metadata supplied by the shiftSelection mixin's selectionMethodFor helper.
File Structure ​
JavaScript Files (.js) ​
mixins/shiftSelection.js— Multi-select mixin used by the trash pagetoggleSelectTrash(item, type, index, event, pageType)— handles single and shift-click selection across the mixed folders+files listtoggleSelectAll()— selects or deselects all visible items (called from the page)recordSelectionMethod(method)— records 'individual', 'shift', or 'select-all' for analyticsselectionMethodFor(count)— returns the Amplitude-ready selection method metadata objectcascadeSize(folders)— returns aggregate asset+subfolder counts for selected folders
mixins/imageStyle.js— Dynamic image orientation mixingetSrcPath(file)— returns the correct thumbnail or fallback for audio/video/image assetsassetsListOnloadSetStyle(file, stylesObj, refPrefix)— sets portrait/landscape CSS class after image load
mixins/amplitude-analytics.js— Analytics mixindispatchAnalytics(body)— fires an Amplitude eventtrackActivity(body, extra)— enriches and fires an activity event
Vue Component Files (.vue) ​
pages/_workspace_id/dam/trash.vue— Trash page- Manages
files[],folders[], pagination state, sort state, and selection state - Handles all three API operations: list, permanent delete, restore
- Renders both list and grid views with skeleton loaders and infinite scroll
- Manages
components/dam/Collage/FileListingCard.vue— Asset grid card- Accepts
flag="trash-listing"to show Restore option alongside Delete in the context menu - Emits
@restoreand@deleteevents back to the trash page - Handles per-card checkbox selection and shift-key state via
shiftKeyprop
- Accepts
components/dam/Folders/FolderCard.vue— Folder grid card- Accepts
:can-restore="canRestoreTrash"to conditionally show the Restore option - Emits
@restoreand@deleteevents - Displays folder name, asset count, and subfolder count
- Accepts
components/theme/global/Dialog/ConfirmationDialog.vue— Confirm dialog- Used for all four operations: delete selected, restore selected, empty trash, restore all
- Emits
confirmandcancel
SVG Icon Components ​
components/svg/CollageEmptyTrashIcon.vue— "Empty Trash" option in the page menucomponents/svg/CollageRestoreIcon.vue— Restore option in item context menus and bulk toolbarcomponents/svg/CollageDeleteIcon.vue— Delete option in item context menus and bulk toolbarcomponents/svg/CollageOptionIcon.vue— Three-dot context menu trigger (page menu and per-item)components/svg/CollageEmptyIcon.vue— Empty-state illustration when trash has no itemscomponents/svg/CollageFolderLargeIcon.vue— Folder icon in list-view rowscomponents/svg/CollageListViewIcon.vue— List view toggle button iconcomponents/svg/CollageGridViewIcon.vue— Grid view toggle button iconcomponents/svg/CollageTableFilterArrow.vue— Sort direction arrow in list-view column headers
Trash Page ​
File: pages/_workspace_id/dam/trash.vue
Route: /:workspace_id/dam/trash
Features ​
- Lists deleted folders and assets together, paginated at 36 per page
- Infinite scroll: fetches next page when scroll reaches 85% of container height
- List view: sortable columns (Name, Date Deleted, Date Added, File Size, File Type)
- Grid view: card thumbnails for files; folder cards with asset and subfolder counts
- Bulk selection with shift-click range support via
shiftSelectionmixin - Per-item context menu: Delete (admin only) and Restore (manager + admin)
- Bulk toolbar: Restore button (manager + admin) and Delete button (admin only)
- Page-level options menu: "Restore All" and "Empty Trash"
- 30-day expiry notice banner shown when trash is non-empty
- Confirmation dialog for all destructive and restore operations
Props / Data ​
javascript
{
files: [], // Deleted asset objects with is_selected flag
folders: [], // Deleted folder objects with is_selected flag
mode: 'list', // 'list' | 'grid' — view mode
loading: false, // True during initial page load
deleting: false, // True while permanent delete API call is in flight
restoring: false, // True while restore API call is in flight
pageScrolling: false, // True when scroll > 36px (toggles sticky header class)
selectedFiles: [], // Files with is_selected === true
selectedFolders: [], // Folders with is_selected === true
page_sort_value: 'modified_at', // Active sort column key
page_sort_by: 'DESC', // 'ASC' | 'DESC'
currentPage: 0, // Last fetched page number (0 = not started)
lastPage: 1, // Total pages from API response
contentLoading: true, // True until the first page load completes
empty_trash: false, // Flag: "Empty Trash" operation pending confirm
restore_all: false, // Flag: "Restore All" operation pending confirm
delete_selected: false, // Flag: delete-selected operation pending confirm
restore_selected: false, // Flag: restore-selected operation pending confirm
delAsset: null, // Single asset targeted by 3-dot menu delete/restore
delFolder: null, // Single folder targeted by 3-dot menu delete/restore
loadMore: false, // True while fetching additional pages (scroll trigger)
fileCardKey: 1 // Incremented to force checkbox re-render after shift-select
}Computed Properties ​
javascript
{
canManageTrash() // $canManageTrash(workspace) — admin only; gates delete + empty-trash
canRestoreTrash() // $canRestoreTrash(workspace) — manager + admin; gates restore
mainTrashMenu() // Builds page-level options menu from canManageTrash / canRestoreTrash flags
totalAssets() // files.length + folders.length
selectedCount() // selectedFiles.length + selectedFolders.length
selectAllClass() // 'semi-selected' CSS class when some but not all items are checked
delDialogHeader() // 'Confirm Delete' or 'Confirm Restore' derived from operation flags
delDialogMsg() // Descriptive message listing type ('Asset', 'Folder') and count
getDelDialog() // true when any operation flag is set — controls dialog visibility
}Key Methods ​
javascript
{
// Data loading
fetchTrashItems() // Paginated GET; appends results; deduplicates on pages > 1
// Sorting
handleSort(field) // Toggles ASC/DESC for field; resets pagination; calls fetchTrashItems
sortItems(sortField, sortOrder) // Updates sort state, resets page, triggers fetch
// Scrolling
handleScroll(flag) // Updates pageScrolling; triggers fetchTrashItems when near bottom
// Selection
toggleSelect(item, type, index, event) // Delegates to shiftSelection.toggleSelectTrash
toggleSelectAll() // Selects/deselects all files and folders; updates selectedFiles/Folders
// Dialog flow
showConfirmationDialog(flag, items, type) // Sets operation flag; sets delAsset/delFolder for single items
closeDelDialog() // Resets all operation flags and delAsset/delFolder
submitDelDialog() // Routes to deleteTrashItems or restoreTrashItems based on flags
// Operations
deleteTrashItems(flag) // POST permanent-delete; removes from files[]/folders[] locally
restoreTrashItems(flag) // POST restore; removes from files[]/folders[] locally
selectionClick(flag) // Guards against concurrent ops; calls showConfirmationDialog
// View
changeView(data) // Sets mode: 'list' | 'grid'
}Workflows ​
Soft-Delete Flow (Initiated Elsewhere) ​
1. User deletes an asset or folder from DAM (folders detail, search, etc.)
Other page calls the relevant delete endpoint
Backend: sets deleted_at timestamp on the asset/category record
Asset/folder disappears from normal DAM views
↓
2. Item appears in trash
GET digital-assets/category/get-deleted-category-with-files
Returns items where deleted_at is set and permanent delete has not run
Trash page displays item with Date Deleted = modified_at (the soft-delete timestamp)
↓
3. 30-day countdown begins
Backend scheduled job permanently removes items after 30 days
Frontend shows info banner: 'Assets in trash are deleted forever after 30 days.'Load Workflow ​
1. User navigates to trash
Route: /:workspace_id/dam/trash
Middleware: authCheck → check-workspace-access → checkWorkspace
→ can-access-dam-module → check-if-suspended
Layout: damLayout
↓
2. mounted() calls fetchTrashItems()
currentPage incremented to 1
contentLoading: true shows skeleton loaders (list or grid pattern)
↓
3. GET digital-assets/category/get-deleted-category-with-files
Params: { workspace_id, page: 1, total_record: 36, sort_value: 'modified_at', sort_by: 'DESC' }
Response: assets_with_folder.data[] — mixed item_type='folder'|'file' array
↓
4. Items processed
item_type === 'folder' → pushed to folders[] with { folder_name, total_assets, sub_category_count }
Otherwise → pushed to files[] with { display_file_name, file_size, file_type, modified_at }
All items get is_selected: false
lastPage set from assets_with_folder.last_page
↓
5. $nextTick sets contentLoading: false — real content renders
If files.length === 0 && folders.length === 0: empty state shown
Otherwise: list or grid view renders based on modeRestore Workflow ​
1. User selects items (checkbox, shift-click, or Restore All)
OR clicks Restore from a single item's 3-dot context menu
↓
2. showConfirmationDialog('restore-selected'|'restore-all', items, type)
Sets restore_selected or restore_all flag
For single 3-dot menu: sets delAsset or delFolder to the item object
getDelDialog computed becomes true → ConfirmationDialog shown
↓
3. Dialog message computed
delDialogMsg: 'Are you sure you want to restore the selected Asset/Folder?'
↓
4. User confirms → submitDelDialog() → restoreTrashItems(flag)
restoring: true (shows spinner in bulk toolbar restore button)
↓
5. POST digital-assets/category/restore-deleted-category-with-files
Body: { workspace_id, category_ids: [...], assets_ids: [...] }
IDs resolved from: single item, selected items, or all items based on flag
↓
6. On success
snackbar.success(message)
Amplitude: 'trash restored' event with asset_id[], category_id[], counts, selection method
Restored items filtered out of files[] and folders[] locally
selectedFiles / selectedFolders updated to remove restored items
restoring: false
closeDelDialog() resets all flagsPermanent Delete Workflow ​
1. User selects items (checkbox, shift-click, or Empty Trash)
OR clicks Delete from a single item's 3-dot context menu
↓
2. showConfirmationDialog('delete-selected'|'delete-all', items, type)
Sets delete_selected or empty_trash flag
For single 3-dot menu: sets delAsset or delFolder
getDelDialog becomes true → ConfirmationDialog shown
↓
3. Dialog message computed
delDialogMsg: 'Are you sure you want to permanently delete the selected Asset/Folder?'
↓
4. User confirms → submitDelDialog() → deleteTrashItems(flag)
deleting: true (shows spinner in bulk toolbar delete button)
↓
5. POST digital-assets/category/permanent-delete-category-with-files
Body: { workspace_id, category_ids: [...], assets_ids: [...] }
IDs resolved from: single item, selected items, or all items based on flag
↓
6. On success
snackbar.success(message)
Amplitude: 'trash permanently-deleted' event with ids, counts, cascadeSize, selection method
Deleted items filtered out of files[] and folders[] locally
selectedFiles / selectedFolders pruned accordingly
deleting: false
closeDelDialog() resets all flagsSort Workflow ​
1. User clicks a column header in list view
handleSort(field) called
sort_loader = field → progress spinner shown in that column header
↓
2. Direction toggled
If page_sort_value === field: flip ASC ↔ DESC
Otherwise: keep current direction, switch field
↓
3. sortItems(field, direction) called
page_sort_value = field
page_sort_by = direction.toUpperCase()
scrollContainer.scrollTop reset to 0
showArrowSort object updated: only new field = true
select_all = false, selectedFiles = [], selectedFolders = []
currentPage = 0, lastPage = 1 — pagination reset
loading: true
↓
4. fetchTrashItems() called with new sort params
sort_loader = '' on completionInfinite Scroll Workflow ​
1. User scrolls scroll container or list body
handleScroll(flag) fires on scroll event
↓
2. Scroll proximity checked
(scrollTop + clientHeight) / scrollHeight >= 0.85
↓
3. If nearBottom and not loading and not loadMore and currentPage < lastPage
fetchTrashItems() called
loadMore: true — skeleton rows appended at bottom of list/grid
currentPage incremented
New items deduplicated and appended to files[] and folders[]
loadMore: false on completionAPI Integration ​
Trash Endpoints ​
| Method | Endpoint | Description |
|---|---|---|
GET | /digital-assets/category/get-deleted-category-with-files | Paginated list of deleted folders and assets |
POST | /digital-assets/category/permanent-delete-category-with-files | Permanently delete selected items |
POST | /digital-assets/category/restore-deleted-category-with-files | Restore selected items to their original location |
Request and Response Examples ​
GET /digital-assets/category/get-deleted-category-with-files ​
javascript
// Request
GET /digital-assets/category/get-deleted-category-with-files
?workspace_id=42
&page=1
&total_record=36
&sort_value=modified_at
&sort_by=DESC
// Response
{
"data": {
"assets_with_folder": {
"data": [
{
"id": 200,
"item_type": "folder",
"folder_name": "Archive 2023",
"assets_count": 14,
"sub_categories_count": 2,
"created_at": "2023-06-01T10:00:00Z",
"modified_at": "2024-01-10T15:30:00Z"
},
{
"id": 301,
"item_type": "file",
"display_file_name": "hero-banner.png",
"file_type": "png",
"file_size": 2097152,
"thumbnail_file": "https://cdn.example.com/thumbs/hero-banner.jpg",
"created_at": "2023-12-15T08:00:00Z",
"modified_at": "2024-01-10T14:00:00Z"
}
],
"current_page": 1,
"last_page": 3,
"total": 86
}
}
}POST /digital-assets/category/permanent-delete-category-with-files ​
javascript
// Request
POST /digital-assets/category/permanent-delete-category-with-files
{
"workspace_id": 42,
"category_ids": [200],
"assets_ids": [301, 302]
}
// Response
{
"message": "Items permanently deleted successfully."
}POST /digital-assets/category/restore-deleted-category-with-files ​
javascript
// Request
POST /digital-assets/category/restore-deleted-category-with-files
{
"workspace_id": 42,
"category_ids": [],
"assets_ids": [301]
}
// Response
{
"message": "Items restored successfully."
}Component Integration ​
Using FileListingCard and FolderCard with Trash Flag ​
vue
<template>
<div>
<!-- Info banner -->
<div v-if="files.length || folders.length" class="notes info mb-2">
<p>Assets in trash are deleted forever after 30 days.</p>
</div>
<!-- Grid view — folders first, then files -->
<v-row no-gutters class="collage-grid-lists row-gap-24">
<v-col
v-for="(count, index) in totalAssets"
:key="'grid_' + count"
cols="4"
lg="3"
xl="2"
class="collage-grid-item collage-mosaic-grid-view"
>
<template v-if="folders.length > index">
<FolderCard
:key="count + '-folder'"
:folder="folders[index]"
:total_assets="folders[index].total_assets"
:total_category="folders[index].sub_category_count"
:enableCheckbox="true"
flag="trash-listing"
:can-restore="canRestoreTrash"
@fileSelect="toggleSelect(folders[index], 'folder', index, $event)"
@delete="confirmDelete([folders[index]], 'folder')"
@restore="confirmRestore([folders[index]], 'folder')"
/>
</template>
<template v-else>
<FileListingCard
:key="files[index - folders.length].id + '-trash'"
:file="files[index - folders.length]"
flag="trash-listing"
:can-restore="canRestoreTrash"
@fileSelect="toggleSelect(files[index - folders.length], 'file', index - folders.length, $event)"
@delete="confirmDelete([files[index - folders.length]], 'file')"
@restore="confirmRestore([files[index - folders.length]], 'file')"
/>
</template>
</v-col>
</v-row>
<!-- Confirmation dialog -->
<DeleteDialog
:heading="dialogHeading"
:msg="dialogMsg"
:dialog="showDialog"
@confirm="handleConfirm"
@cancel="showDialog = false"
/>
</div>
</template>
<script>
import imageStyle from '~/mixins/imageStyle'
import shiftSelection from '~/mixins/shiftSelection'
export default {
name: 'TrashGrid',
mixins: [imageStyle, shiftSelection],
components: {
FileListingCard: () => import('~/components/dam/Collage/FileListingCard.vue'),
FolderCard: () => import('~/components/dam/Folders/FolderCard.vue'),
DeleteDialog: () => import('~/components/theme/global/Dialog/ConfirmationDialog.vue'),
},
data() {
return {
files: [],
folders: [],
showDialog: false,
dialogHeading: '',
dialogMsg: '',
pendingOperation: null, // { type: 'delete'|'restore', items, itemType }
lastSelectedIndex: -1,
fileCardKey: 1,
selectedFiles: [],
selectedFolders: [],
}
},
computed: {
totalAssets() {
return this.files.length + this.folders.length
},
canRestoreTrash() {
const workspaceId = this.$route.params.workspace_id || this.$getWorkspaceId()
const workspace = this.$auth.user?.accessibleWorkspaces?.find(
({ id }) => parseInt(id) === parseInt(workspaceId)
)
return workspace ? this.$canRestoreTrash(workspace) : false
},
},
async mounted() {
await this.loadTrash()
},
methods: {
async loadTrash() {
try {
const response = await this.$axios.$get(
'digital-assets/category/get-deleted-category-with-files?' +
this.$toQueryString({
workspace_id: this.$getWorkspaceId(),
page: 1,
total_record: 36,
sort_value: 'modified_at',
sort_by: 'DESC',
})
)
const assets = response.data?.assets_with_folder?.data || []
this.folders = assets
.filter((a) => a.item_type === 'folder')
.map((a) => ({ ...a, is_selected: false, total_assets: a.assets_count || 0, sub_category_count: a.sub_categories_count || 0 }))
this.files = assets
.filter((a) => a.item_type !== 'folder')
.map((a) => ({ ...a, is_selected: false }))
} catch (error) {
this.$snackbar.error(this.$getErrorMessage(error))
}
},
toggleSelect(item, type, index, event) {
this.toggleSelectTrash(item, type, index, event, 'trash')
},
confirmDelete(items, itemType) {
this.pendingOperation = { type: 'delete', items, itemType }
this.dialogHeading = 'Confirm Delete'
this.dialogMsg = `Are you sure you want to permanently delete the selected ${itemType === 'folder' ? 'Folder' : 'Asset'}?`
this.showDialog = true
},
confirmRestore(items, itemType) {
this.pendingOperation = { type: 'restore', items, itemType }
this.dialogHeading = 'Confirm Restore'
this.dialogMsg = `Are you sure you want to restore the selected ${itemType === 'folder' ? 'Folder' : 'Asset'}?`
this.showDialog = true
},
async handleConfirm() {
this.showDialog = false
const { type, items, itemType } = this.pendingOperation
const ids = items.map(({ id }) => id)
const endpoint =
type === 'delete'
? 'digital-assets/category/permanent-delete-category-with-files'
: 'digital-assets/category/restore-deleted-category-with-files'
try {
const { message } = await this.$axios.$post(endpoint, {
workspace_id: this.$getWorkspaceId(),
category_ids: itemType === 'folder' ? ids : [],
assets_ids: itemType === 'file' ? ids : [],
})
this.$snackbar.success(message)
// Remove from local list
if (itemType === 'folder') {
this.folders = this.folders.filter((f) => !ids.includes(f.id))
} else {
this.files = this.files.filter((f) => !ids.includes(f.id))
}
} catch (error) {
this.$snackbar.error(this.$getErrorMessage(error))
}
this.pendingOperation = null
},
},
}
</script>Related Documentation ​
- Dashboard — Entry point where assets and collages first appear
- File Upload — How assets are created and may later be deleted
- Mixins — Shift Selection — Multi-select and range-select logic
- Mixins — Image Style — Thumbnail orientation class helpers
- Mixins — Amplitude Analytics — Analytics event tracking