Skip to content

Dashboard ​

Overview ​

The DAM dashboard is the home screen after workspace login. It loads five parallel data streams using TanStack Query and presents them as distinct UI sections:

  1. Recent Collages — the six most recently updated collages in the workspace, fetched via collectionApi.getRecentsCollages and keyed under queryKeys.dashboard.recentCollages.
  2. Recent Folders — the most recently accessed folders, fetched via folderApi.getRecentsFolders and keyed under queryKeys.dashboard.recentFolders.
  3. Recent Uploads — assets recently uploaded to the workspace, fetched via commonApis.loadRecentUploads and keyed under queryKeys.dashboard.recentUploads.
  4. Weekly Insights — a 7-day rolling activity summary (viewed, added, downloaded, shared counts), fetched via commonApis.fetchWeeklyInsights and keyed under queryKeys.dashboard.weeklyInsights.
  5. Overview — workspace-wide counters (total assets, folders, collages, uncategorized), fetched via commonApis.getOverviewData and keyed under queryKeys.dashboard.overview.

Announcements are loaded separately via useAnnouncementsApi and displayed as a dismissible banner when an active announcement exists.

Architecture ​

The dashboard page is a Nuxt 4 route ([workspace_id]/dam/dashboard.vue) that mounts useDashboardQuery to coordinate all five TanStack Query instances. All queries share an enabled guard that prevents execution until a valid workspace ID is resolved from useWorkspaceId. The weekly insights window is computed once on mount: toDate is today, fromDate is six days prior. Cache invalidation methods (invalidateRecentCollages, invalidateRecentFolders, refetchOverview) are exposed so event-driven refreshes (e.g., after an upload or collage create) can target only the affected slice without a full page reload. The dam.ts Pinia store holds workspace-level state (folder list, storage info, DAM instance settings) that the dashboard shares with the broader DAM shell; the dashboard's five queries are independent of the store and managed entirely by TanStack Query.

File Structure ​

Composable Files ​

  • composables/queries/useDashboardQuery.ts — coordinates all five dashboard queries in a single composable; exposes data refs and targeted invalidation helpers
  • composables/api/useCommonApis.ts — provides loadRecentUploads, fetchWeeklyInsights, and getOverviewData used by the dashboard query
  • composables/api/useFolderApi.ts — provides getRecentsFolders for the Recent Folders section
  • composables/api/useCollectionApi.ts — provides getRecentsCollages for the Recent Collages section
  • composables/api/useAnnouncementsApi.ts — provides listAnnouncements, addAnnouncement, updateAnnouncement, deleteAnnouncement for the announcements banner
  • composables/queries/useAnnouncementsQuery.ts — infinite TanStack Query for the announcements list; filters by publishType, searchTerm, sortValue, sortBy

Store Files ​

  • stores/dam.ts — Pinia store (useDamStore); holds workspace storage info, folder list, DAM instance settings, and upload state shared with the DAM shell

Type Files ​

  • types/dashboard.ts — exports Collage, RecentUpload, WeeklyInsight types used by the dashboard query
  • types/common.ts — exports OverviewData type returned by getOverviewData
  • types/notification.ts — exports Announcement and related types for the announcements system

Page Files ​

  • app/pages/[workspace_id]/dam/dashboard/index.vue — DAM dashboard page; quick-action cards, Recents tabbed card, weekly insights chart, and recently added asset list

Page Details ​

Dashboard ([workspace_id]/dam/dashboard/index.vue) ​

FieldValue
Route/:workspace_id/dam/dashboard
Layoutcollage-layout
Middlewareauth-check, check-workspace-access, check-workspace, can-access-dam-module, check-if-suspended
Primary composablesuseDashboard(), useImageStyle(), useHelpers()

Sections

SectionDescription
Quick-action cardsUpload, Browse, Search, Collages, Portals; the Storage card is gated by subscription plan
Recents tabbed cardTwo tabs — Collages and Folders — each showing the most recently accessed items
Last 7 DaysInsightsTabGraph component showing weekly asset-view counts
Recently AddedList of the most recently uploaded assets with thumbnails

Dialogs (all rendered client-only): ShareAssetDialog, ConfirmationDialog, PermissionDialog, CreateOrRenameDialog


useDashboardQuery ​

The primary composable for the dashboard page. Instantiates and returns five TanStack Query instances plus cache invalidation helpers.

Features ​

  • All five queries share a single enabled computed guard on workspace ID resolution
  • Weekly insight date window is computed once at composable creation time (today back 6 days)
  • Preview images in the collages query receive unique UUIDs to prevent Vue keying collisions
  • Exposes invalidateRecentCollages, invalidateRecentFolders, and refetchOverview for event-driven UI refreshes after mutations

Return Value ​

javascript
{
  collagesQuery: UseQueryReturnType<Collage[]>,
  foldersQuery: UseQueryReturnType<Folder[]>,
  uploadsQuery: UseQueryReturnType<RecentUpload[]>,
  insightsQuery: UseQueryReturnType<Record<string, WeeklyInsight>>,
  overviewQuery: UseQueryReturnType<OverviewData | undefined>,
  invalidateRecentCollages: () => Promise<void>,
  invalidateRecentFolders: () => Promise<void>,
  refetchOverview: () => Promise<void>
}

Usage Example ​

vue
<script setup lang="ts">
const {
  collagesQuery,
  foldersQuery,
  uploadsQuery,
  insightsQuery,
  overviewQuery,
  invalidateRecentCollages,
} = useDashboardQuery()

// Listen for collage creation event and refresh only that slice
const mittBus = useMittBus()
onMounted(() => {
  mittBus?.on('collage-created', invalidateRecentCollages)
})
onBeforeUnmount(() => {
  mittBus?.off('collage-created', invalidateRecentCollages)
})
</script>

<template>
  <div>
    <div v-if="overviewQuery.isPending.value">Loading overview...</div>
    <div v-else>
      <p>Total assets: {{ overviewQuery.data.value?.total_assets }}</p>
      <p>Total folders: {{ overviewQuery.data.value?.total_folders }}</p>
      <p>Total collages: {{ overviewQuery.data.value?.total_collages }}</p>
    </div>

    <div v-if="collagesQuery.isPending.value">Loading collages...</div>
    <div v-else class="collage-grid">
      <CollageCard
        v-for="collage in collagesQuery.data.value"
        :key="collage.id"
        :collage="collage"
      />
    </div>
  </div>
</template>

useAnnouncementsQuery ​

Infinite TanStack Query for workspace announcements, used both by the dashboard banner and the Notifications settings page.

Options ​

javascript
{
  workspaceId: Ref<string>,    // required — gates the query
  searchTerm: Ref<string>,     // filters by keyword
  publishType: Ref<string>,    // 'published' | 'draft' | ''
  sortValue: Ref<string>,      // field to sort by
  sortBy: Ref<string>          // 'ASC' | 'DESC'
}

Return Value ​

javascript
{
  announcements: ComputedRef<Announcement[]>,   // flattened across all pages
  isPending: Ref<boolean>,
  isFetchingNextPage: Ref<boolean>,
  hasNextPage: Ref<boolean>,
  error: Ref<Error | null>,
  fetchNextPage: () => void,
  refetch: () => void
}

Usage Example ​

vue
<script setup lang="ts">
const workspaceId = computed(() => getWorkspaceId() ?? '')
const publishType = ref('published')
const searchTerm = ref('')
const sortValue = ref('created_at')
const sortBy = ref('DESC')

const { announcements, isPending } = useAnnouncementsQuery({
  workspaceId,
  searchTerm,
  publishType,
  sortValue,
  sortBy,
})
</script>

<template>
  <div v-if="announcements.length" class="announcement-banner">
    <p>{{ announcements[0]?.message }}</p>
  </div>
</template>

useAnnouncementsApi ​

CRUD composable for the announcements system. All methods are wrapped in track() for loading/error state tracking.

Methods ​

javascript
{
  listAnnouncements: (params: AnnouncementListParams) => Promise<ApiResponse<AnnouncementListResponse>>,
  getAnnouncementUsers: (workspaceId: string | number, moduleId?: number) => Promise<ApiResponse<AnnouncementUser[]>>,
  addAnnouncement: (payload: AddAnnouncementPayload) => Promise<ApiResponse>,
  updateAnnouncement: (payload: UpdateAnnouncementPayload) => Promise<ApiResponse>,
  deleteAnnouncement: (params: DeleteAnnouncementParams) => Promise<ApiResponse>,
  isLoading: Ref<boolean>,
  error: Ref<Error | null>
}

Workflows ​

Dashboard Load Workflow ​

User navigates to /:workspace_id/dam/dashboard
  → useWorkspaceId() resolves workspace ID
  → enabled computed becomes true
  → useDashboardQuery mounts 5 parallel queries:
      → collagesQuery    → GET digital-assets/new-dashboard/recently-collection
      → foldersQuery     → GET digital-assets/new-dashboard/recently-folders
      → uploadsQuery     → GET digital-assets/new-dashboard/recently-uploaded-assets
      → insightsQuery    → POST digital-assets/new-dashboard/weekly-insights
      → overviewQuery    → GET digital-assets/dashboard/common-data
  → Each query populates its section independently
  → Announcements query loads separately
  → Dashboard renders progressively as each query resolves

Cache Invalidation After Mutation ​

User creates a collage (from dashboard quick action)
  → CollageCreate mutation succeeds
  → mittBus emits 'collage-created'
  → invalidateRecentCollages() called
  → queryClient.invalidateQueries({ queryKey: queryKeys.dashboard.recentCollages(workspaceId) })
  → TanStack Query refetches collagesQuery in background
  → Dashboard collage section updates without full page reload

API Integration ​

Endpoints ​

EndpointMethodDescription
digital-assets/new-dashboard/recently-collectionGETReturns recent collages for the workspace
digital-assets/new-dashboard/recently-foldersGETReturns recently accessed folders
digital-assets/new-dashboard/recently-uploaded-assetsGETReturns recently uploaded assets
digital-assets/new-dashboard/weekly-insightsPOSTReturns 7-day activity counts
digital-assets/dashboard/common-dataGETReturns workspace overview counters
announcement/listPOSTPaginated announcements list
announcement/addPOSTCreate new announcement
announcement/updatePOSTUpdate existing announcement
announcement/deleteGETDelete announcement by ID

Weekly Insights Request ​

json
{
  "date_type": "week",
  "to_date": "2025-08-10",
  "from_date": "2025-08-04"
}

Weekly Insights Response ​

json
{
  "data": {
    "viewed": { "count": 142, "change": 12 },
    "added": { "count": 37, "change": -5 },
    "downloaded": { "count": 28, "change": 3 },
    "shared": { "count": 9, "change": 0 }
  }
}

Overview Response ​

json
{
  "data": {
    "total_uncategorized": 14,
    "total_assets": 3842,
    "total_folders": 67,
    "total_collages": 22,
    "white_label_enabled": false,
    "smtp_configured": true
  }
}

Component Integration ​

The dashboard page ([workspace_id]/dam/dashboard/index.vue) delegates all data fetching, state management, and action handling to the useDashboard() facade composable. The page destructures everything it needs in a single call — it never calls useDashboardQuery() directly.

vue
<script setup lang="ts">
const {
  topBtnRoutes,
  contentLoad,
  storageFull,
  navigateRoute,
  tab,
  tabLabels,
  collagesLoading,
  collages,
  shareCollection,
  openRenameDialog,
  openPermissions,
  confirmDeleteCollection,
  openCollageFolder,
  addCollage,
  foldersLoading,
  addFolderDialog,
  folderList,
  weeklyInsightLoading,
  weeklyInsightList,
  InsightsBarOptions,
  InsightBarPlugin,
  recentLoading,
  recentUploadList,
  workspace_id,
  openAsset,
  removeFileExtension,
  shareDialog,
  collection,
  deleteDialog,
  deleteCollection,
  permissionDialog,
  permission_items,
  onPermissionUpdated,
  flag,
  renameDialog,
  changeCollectionName,
  collageActions,
  onCollageNameChange,
  resetPermissionItems,
} = useDashboard() as ReturnType<typeof useDashboard>

const { getSrcPath } = useImageStyle()
const { formatDate } = useHelpers()
</script>

<template>
  <div class="collage-body flex-column">
    <!-- Quick action buttons: New Asset / New Collage / New Folder -->
    <v-row v-if="contentLoad || topBtnRoutes.length" class="my-0 flex-0-0">
      <v-col v-for="topBtn in topBtnRoutes" :key="topBtn.title">
        <div class="dashboard-boxs" @click="navigateRoute(topBtn)">
          <v-avatar :color="topBtn.color"><AsyncIcon :name="topBtn.icon" /></v-avatar>
          <span>{{ topBtn.title }}</span>
        </div>
      </v-col>
    </v-row>

    <v-row class="my-0 flex-1-1">
      <!-- Recents panel: Collages tab / Folders tab -->
      <v-col cols="12" lg="8">
        <v-card class="boxview">
          <v-card-title>
            <v-tabs v-model="tab">
              <v-tab v-for="(label, i) in tabLabels" :key="i">{{ label }}</v-tab>
            </v-tabs>
          </v-card-title>
          <v-window v-model="tab">
            <v-window-item :value="0">
              <CardSkeleton v-if="collagesLoading" />
              <v-row v-else-if="collages.length">
                <v-col v-for="(item, i) in collages" :key="i" cols="12" md="6" lg="4">
                  <Card
                    :collage="item"
                    :flag="flag"
                    @share="shareCollection(item)"
                    @edit="openRenameDialog(item)"
                    @permissions="openPermissions(item)"
                    @delete="confirmDeleteCollection(item)"
                    @open="openCollageFolder(item)"
                  />
                </v-col>
              </v-row>
            </v-window-item>
            <v-window-item :value="1">
              <RecentFolderListItem
                v-for="folder in folderList"
                :key="folder.id"
                :folder="folder"
              />
            </v-window-item>
          </v-window>
        </v-card>
      </v-col>

      <!-- Right column: Weekly Insights + Recently Added -->
      <v-col cols="12" lg="4">
        <InsightsTabGraph
          heading="Last 7 Days"
          :data-loading="weeklyInsightLoading"
          :weekly-insight-list="weeklyInsightList"
          :bar-options="InsightsBarOptions"
        />
        <v-card class="boxview recently-uploaded">
          <v-card-title><h4>Recently Added</h4></v-card-title>
          <v-card-text>
            <RecentUploadListItem
              v-for="recent in recentUploadList"
              :key="recent.id"
              :recent="recent"
              @open-asset="openAsset(recent.id, true)"
            />
          </v-card-text>
        </v-card>
      </v-col>
    </v-row>

    <!-- Action dialogs (rendered client-side only) -->
    <client-only>
      <ShareAssetDialog :dialog="shareDialog" :collection="collection" />
      <ConfirmationDialog :dialog="deleteDialog" @confirm="deleteCollection" />
      <PermissionDialog
        :dialog="permissionDialog"
        :items="permission_items"
        @updated="onPermissionUpdated"
        @reset="resetPermissionItems"
      />
      <CreateOrRenameDialog
        :dialog="renameDialog"
        @submit="changeCollectionName"
        @input="onCollageNameChange"
      />
    </client-only>
  </div>
</template>

useDashboard() internally calls useDashboardQuery() — the page never interacts with the query composable directly.