Appearance
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:
- Recent Collages — the six most recently updated collages in the workspace, fetched via
collectionApi.getRecentsCollagesand keyed underqueryKeys.dashboard.recentCollages. - Recent Folders — the most recently accessed folders, fetched via
folderApi.getRecentsFoldersand keyed underqueryKeys.dashboard.recentFolders. - Recent Uploads — assets recently uploaded to the workspace, fetched via
commonApis.loadRecentUploadsand keyed underqueryKeys.dashboard.recentUploads. - Weekly Insights — a 7-day rolling activity summary (viewed, added, downloaded, shared counts), fetched via
commonApis.fetchWeeklyInsightsand keyed underqueryKeys.dashboard.weeklyInsights. - Overview — workspace-wide counters (total assets, folders, collages, uncategorized), fetched via
commonApis.getOverviewDataand keyed underqueryKeys.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 helperscomposables/api/useCommonApis.ts— providesloadRecentUploads,fetchWeeklyInsights, andgetOverviewDataused by the dashboard querycomposables/api/useFolderApi.ts— providesgetRecentsFoldersfor the Recent Folders sectioncomposables/api/useCollectionApi.ts— providesgetRecentsCollagesfor the Recent Collages sectioncomposables/api/useAnnouncementsApi.ts— provideslistAnnouncements,addAnnouncement,updateAnnouncement,deleteAnnouncementfor the announcements bannercomposables/queries/useAnnouncementsQuery.ts— infinite TanStack Query for the announcements list; filters bypublishType,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— exportsCollage,RecentUpload,WeeklyInsighttypes used by the dashboard querytypes/common.ts— exportsOverviewDatatype returned bygetOverviewDatatypes/notification.ts— exportsAnnouncementand 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) ​
| Field | Value |
|---|---|
| Route | /:workspace_id/dam/dashboard |
| Layout | collage-layout |
| Middleware | auth-check, check-workspace-access, check-workspace, can-access-dam-module, check-if-suspended |
| Primary composables | useDashboard(), useImageStyle(), useHelpers() |
Sections
| Section | Description |
|---|---|
| Quick-action cards | Upload, Browse, Search, Collages, Portals; the Storage card is gated by subscription plan |
| Recents tabbed card | Two tabs — Collages and Folders — each showing the most recently accessed items |
| Last 7 Days | InsightsTabGraph component showing weekly asset-view counts |
| Recently Added | List 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
enabledcomputed 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, andrefetchOverviewfor 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 resolvesCache 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 reloadAPI Integration ​
Endpoints ​
| Endpoint | Method | Description |
|---|---|---|
digital-assets/new-dashboard/recently-collection | GET | Returns recent collages for the workspace |
digital-assets/new-dashboard/recently-folders | GET | Returns recently accessed folders |
digital-assets/new-dashboard/recently-uploaded-assets | GET | Returns recently uploaded assets |
digital-assets/new-dashboard/weekly-insights | POST | Returns 7-day activity counts |
digital-assets/dashboard/common-data | GET | Returns workspace overview counters |
announcement/list | POST | Paginated announcements list |
announcement/add | POST | Create new announcement |
announcement/update | POST | Update existing announcement |
announcement/delete | GET | Delete 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.