Skip to content

Trash ​

Overview ​

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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 page

    • toggleSelectTrash(item, type, index, event, pageType) — handles single and shift-click selection across the mixed folders+files list
    • toggleSelectAll() — selects or deselects all visible items (called from the page)
    • recordSelectionMethod(method) — records 'individual', 'shift', or 'select-all' for analytics
    • selectionMethodFor(count) — returns the Amplitude-ready selection method metadata object
    • cascadeSize(folders) — returns aggregate asset+subfolder counts for selected folders
  • mixins/imageStyle.js — Dynamic image orientation mixin

    • getSrcPath(file) — returns the correct thumbnail or fallback for audio/video/image assets
    • assetsListOnloadSetStyle(file, stylesObj, refPrefix) — sets portrait/landscape CSS class after image load
  • mixins/amplitude-analytics.js — Analytics mixin

    • dispatchAnalytics(body) — fires an Amplitude event
    • trackActivity(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
  • components/dam/Collage/FileListingCard.vue — Asset grid card

    • Accepts flag="trash-listing" to show Restore option alongside Delete in the context menu
    • Emits @restore and @delete events back to the trash page
    • Handles per-card checkbox selection and shift-key state via shiftKey prop
  • components/dam/Folders/FolderCard.vue — Folder grid card

    • Accepts :can-restore="canRestoreTrash" to conditionally show the Restore option
    • Emits @restore and @delete events
    • Displays folder name, asset count, and subfolder count
  • components/theme/global/Dialog/ConfirmationDialog.vue — Confirm dialog

    • Used for all four operations: delete selected, restore selected, empty trash, restore all
    • Emits confirm and cancel

SVG Icon Components ​

  • components/svg/CollageEmptyTrashIcon.vue — "Empty Trash" option in the page menu
  • components/svg/CollageRestoreIcon.vue — Restore option in item context menus and bulk toolbar
  • components/svg/CollageDeleteIcon.vue — Delete option in item context menus and bulk toolbar
  • components/svg/CollageOptionIcon.vue — Three-dot context menu trigger (page menu and per-item)
  • components/svg/CollageEmptyIcon.vue — Empty-state illustration when trash has no items
  • components/svg/CollageFolderLargeIcon.vue — Folder icon in list-view rows
  • components/svg/CollageListViewIcon.vue — List view toggle button icon
  • components/svg/CollageGridViewIcon.vue — Grid view toggle button icon
  • components/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 shiftSelection mixin
  • 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 mode

Restore 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 flags

Permanent 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 flags

Sort 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 completion

Infinite 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 completion

API Integration ​

Trash Endpoints ​

MethodEndpointDescription
GET/digital-assets/category/get-deleted-category-with-filesPaginated list of deleted folders and assets
POST/digital-assets/category/permanent-delete-category-with-filesPermanently delete selected items
POST/digital-assets/category/restore-deleted-category-with-filesRestore 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>