Skip to content

Activity Log ​

Overview ​

  1. Per-user activity dialog: ActivityLogsDialog is a Vuetify dialog component that shows an infinite-scroll list of activity log entries for a specific portal user. It is not a standalone page — it is launched from the portal users management list.
  2. Triggered from context menu: The portal users page (workspace-settings/portals/users) exposes an "Activity Log" action in each user's row menu. Selecting it opens the dialog with the chosen user pre-loaded.
  3. Direct API call: The component calls the backend endpoint digital-assets/instance/user-activity directly via $api — there is no dedicated useAuditLogApi composable.
  4. Infinite scroll pagination: Entries are fetched one page at a time. Scrolling to 85% of the container depth triggers the next page load without a manual "Load more" button.
  5. Redirect links: Log entries that carry an activity_url field display an external-link icon. Clicking it opens the linked resource in a new tab.
  6. Soft reset on re-open: Each time the dialog opens, it resets page, lastPage, and the entries array before fetching fresh data.

Architecture ​

The activity log feature is a single self-contained dialog component. State — pagination cursors, the entries array, loading flags — is held locally with ref; no Pinia store is involved. The component uses the global $api client (provided by the fetch plugin) and reads route.params.workspace_id for the workspace context. The consuming page passes a user prop containing the portal user record; the dialog extracts user.id to scope the API call. Skeleton loaders cover both the initial load and incremental page fetches so the UI never shows a hard blank state.

File Structure ​

Vue Component Files ​

  • app/components/dam/Dialogs/Org-Settings/ActivityLogsDialog.vue — dialog that renders the paginated activity log for a single portal user

Page Files ​

  • app/pages/[workspace_id]/workspace-settings/portals/users/index.vue — portal users list page; triggers ActivityLogsDialog via the per-row context menu "Activity Log" action

Page Details ​

Portal Users (workspace-settings/portals/users/index.vue) ​

FieldValue
Route/:workspace_id/workspace-settings/portals/users
Layoutgeneral-settings-layout
Middlewareauth-check, check-workspace, can-access-dam-settings, check-if-suspended
Primary composableusePortalUsersPageSettings()

Three status tabs — Active Users, Pending Users, Deactive Users — controlled by filterSelectModel. Searchable by name or email, with infinite scroll and sort by name and email. Subscription-gated "Add New User" button. Last Login and Last Activity columns appear only on the Active Users tab.

List columns: Name, Email, Portals, Last Login (active tab only), Last Activity (active tab only)

Row menu by tab

TabMenu items
ActiveEdit, Activity Log, Reset Password, Deactivate User
PendingResend Invitation, Revoke Access, Activate User, Delete Invitation Record
DeactiveActivate User

Dialogs: ConfirmationDialog, ActivateUserDialog (reset password / activate), UserDialog (add/edit), ActivityLogsDialog

ActivityLogsDialog calls GET digital-assets/instance/user-activity directly, shows a two-column table (Date/Time, Logs), and pages at 85% scroll with silent error handling.


ActivityLogsDialog Component ​

File: app/components/dam/Dialogs/Org-Settings/ActivityLogsDialog.vue

Features ​

  • Opens as a persistent v-dialog at 850 px max-width
  • Skeleton loaders during initial fetch (table-header + 9 row skeletons)
  • Empty state (EmptyStateIcon + "No activity found!" heading) when no entries exist
  • Two-column table: Date / Time and Logs
  • Tooltip on log text for overflow cases
  • External link button per row when activity_url is present
  • Skeleton rows appended at the bottom during incremental page loads
  • Resets all state each time dialog prop flips to true

Props ​

typescript
{
  dialog: boolean        // controls v-model open/close state
  user: Record<string, unknown>  // portal user record; must contain user.id
}

Events ​

typescript
{
  close: []   // emitted when the user dismisses the dialog via the close button
}

Internal State ​

typescript
{
  modalDialog: Ref<boolean>                   // mirrors dialog prop, drives v-dialog v-model
  activities: Ref<Activity[]>                 // accumulated log entries
  isLoading: Ref<boolean>                     // true during initial page 1 fetch
  currentPage: Ref<number>                    // current page cursor (starts at 0)
  lastPage: Ref<number | null>                // last page from API response
  loadMore: Ref<boolean>                      // true during incremental fetches (scroll)
  listLogsContainer: Ref<HTMLElement | null>  // scroll container ref for 85% threshold
}

Key Methods ​

typescript
// Reset pagination and entries before each open
reset(): void

// Dismisses dialog and emits close
closeDialog(): void

// Format a UTC date string as "Jan 1, 2025 3:45 PM"
formatDate(dateStr: string): string

// Fetch one page of entries; flag = 'default' shows full skeleton,
// flag = 'scroll' appends skeleton rows at the bottom
handleFetchActivities(flag?: 'default' | 'scroll'): Promise<void>

// Fires handleFetchActivities('scroll') when container is scrolled past 85%
handleScroll(): Promise<void>

Usage ​

vue
<template>
  <ActivityLogsDialog
    :dialog="activityLogOpen"
    :user="selectedUser"
    @close="activityLogOpen = false"
  />
</template>

<script setup lang="ts">
const activityLogOpen = ref(false)
const selectedUser = ref<Record<string, unknown>>({})

function openActivityLog(user: Record<string, unknown>) {
  selectedUser.value = user
  activityLogOpen.value = true
}
</script>

Workflows ​

Opening the Activity Log for a Portal User ​

1. Admin opens workspace-settings/portals/users
   Page: app/pages/[workspace_id]/workspace-settings/portals/users/index.vue
   ↓
2. Admin clicks the row action menu for a portal user
   Menu item: { key: 'activity-log', label: 'Activity Log' }
   ↓
3. Page sets selectedUser.value = user and activity_log_dialog.value = true
   ↓
4. ActivityLogsDialog receives dialog=true via prop
   Watcher fires → calls reset() → calls handleFetchActivities('default')
   ↓
5. Initial load
   isLoading = true
   currentPage = 1
   API: GET digital-assets/instance/user-activity
   Params: { url_workspace_id, workspace_id, page: 1, user_id }
   ↓
6. Response parsed
   lastPage set from response.data.last_page
   activities populated from response.data.data
   isLoading = false
   ↓
7. Table renders
   Column 1: formatDate(activity.created_at)
   Column 2: activity.log_details with tooltip
   Optional: redirect icon button if activity.activity_url is set
   ↓
8. User scrolls the table body
   handleScroll fires on each scroll event
   scrollPercent = (scrollTop / (scrollHeight - clientHeight)) * 100
   ↓
9. If scrollPercent >= 85 and more pages exist
   handleFetchActivities('scroll') called
   loadMore = true → bottom skeleton rows appear
   Next page fetched and pushed to activities array
   loadMore = false → skeleton rows removed
   ↓
10. User closes dialog
    closeDialog() → modalDialog = false → emit('close') → reset()

API Integration ​

Endpoints ​

MethodEndpointDescription
GETdigital-assets/instance/user-activityPaginated activity log for a single user

Request Parameters ​

json
{
  "url_workspace_id": "abc123",
  "workspace_id": "abc123",
  "page": 1,
  "user_id": 42
}

Response Shape ​

json
{
  "data": {
    "current_page": 1,
    "last_page": 4,
    "data": [
      {
        "id": 1001,
        "created_at": "2025-07-15T14:23:00Z",
        "log_details": "Downloaded asset product-hero.png",
        "activity_url": "/workspace/abc123/dam/files/555"
      },
      {
        "id": 1000,
        "created_at": "2025-07-14T09:10:00Z",
        "log_details": "Viewed portal Brand Assets",
        "activity_url": null
      }
    ]
  }
}

Component Integration ​

ActivityLogsDialog is embedded in app/pages/[workspace_id]/workspace-settings/portals/users/index.vue. All page state — dialog flags, selected user, user list — comes from usePortalUsersPageSettings(). The page does not use v-data-table; it uses DamSettingsListView with a resolve-menu-items callback and a @menu-click handler.

The dialog state variable is activity_log_dialog (snake_case, matching the composable). Menu items are built per-row by buildUserMenu(), which returns different actions depending on the active filter tab (active-users, pending-users, or deactive-users). The activity-log action only appears in the active-users tab.

vue
<!-- app/pages/[workspace_id]/workspace-settings/portals/users/index.vue -->
<template>
  <DamSettingsListView
    :items="portalUserItems"
    :resolve-menu-items="(item) => buildUserMenu(item)"
    @menu-click="handleMenuClick"
    @scroll="onPortalUsersScroll"
  />

  <ActivityLogsDialog
    :dialog="activity_log_dialog"
    :user="selectedUser || {}"
    @close="activity_log_dialog = false"
  />
</template>

<script setup lang="ts">
import type { DamMenuItem } from '~/components/dam/DamItemMenu.vue'
import type { FolderAssetItem } from '~/types/dam-list'
import type { PortalUserRecord } from '~/types/portal'

const {
  activity_log_dialog,
  selectedUser,
  searchResult,
  getSubFlag,
} = usePortalUsersPageSettings()

function buildUserMenu(item: FolderAssetItem): DamMenuItem[] {
  const subFlag = getSubFlag.value
  if (subFlag === 'active-users') {
    return [
      { key: 'edit',            icon: 'editIcon',       label: 'Edit' },
      { key: 'activity-log',    icon: 'eyeIcon',        label: 'Activity Log' },
      { key: 'reset-password',  icon: 'resetPassIcon',  label: 'Reset Password' },
      { key: 'deactivate-user', icon: 'deactivateIcon', label: 'Deactivate User' },
    ]
  }
  if (subFlag === 'pending-users') {
    return [
      { key: 'resend-invitation',        icon: 'resendInviteIcon', label: 'Resend Invitation' },
      { key: 'revoke-access',            icon: 'revokeMailIcon',   label: 'Revoke Access' },
      { key: 'activate-user',            icon: 'checkCircleIcon',  label: 'Activate User' },
      { key: 'delete-invitation-record', icon: 'deleteIcon',       label: 'Delete Invitation Record' },
    ]
  }
  if (subFlag === 'deactive-users') {
    return [{ key: 'activate-user', icon: 'checkCircleIcon', label: 'Activate User' }]
  }
  return []
}

function findPortalUserById(id: FolderAssetItem['id']): PortalUserRecord | undefined {
  return searchResult.value.find((u) => String(u.id) === String(id))
}

function handleMenuClick(item: FolderAssetItem, menuItem: DamMenuItem) {
  const user = findPortalUserById(item.id)
  if (!user) return
  switch (menuItem.key) {
    case 'activity-log':
      selectedUser.value = user
      activity_log_dialog.value = true
      break
    // other cases handled similarly
  }
}
</script>

Portal Users Page Context ​

File: app/pages/[workspace_id]/workspace-settings/portals/users/index.vue

Route: /:workspace_id/workspace-settings/portals/users

The portal users page is the only page that currently embeds ActivityLogsDialog. It manages portal user accounts — inviting, editing, deactivating, resetting passwords, and viewing activity. The page also checks the workspace subscription before allowing new user additions.

Key State ​

typescript
const activityLogOpen = ref(false)       // controls ActivityLogsDialog visibility
const selectedUser = ref<Record<string, unknown> | null>(null)  // user passed to dialog

Row Action Menu Items ​

typescript
[
  { key: 'edit',            icon: 'editIcon',       label: 'Edit' },
  { key: 'activity-log',    icon: 'eyeIcon',        label: 'Activity Log' },
  { key: 'reset-password',  icon: 'resetPassIcon',  label: 'Reset Password' },
  { key: 'deactivate-user', icon: 'deactivateIcon', label: 'Deactivate User' },
]

Subscription-Gated Add User Button ​

The "Add New User" button is disabled with a tooltip when the workspace has reached its portal user limit:

vue
<v-tooltip :disabled="canAddMoreUsers" bottom>
  <template #activator="{ props: tooltipProps }">
    <v-btn
      v-bind="tooltipProps"
      :disabled="!canAddMoreUsers"
      @click="addNewUser('open')"
    >
      Add New User
    </v-btn>
  </template>
  <span>Please upgrade your subscription plan to add more users</span>
</v-tooltip>

Portal Users Filtering ​

The page provides two controls for browsing users:

  • Status filter (v-select): dropdown filter (e.g. active, pending, deactivated) bound to filterSelectModel
  • Search field (v-text-field): filters by name or email, bound to searchTerm

Both controls are disabled while a fetch is in progress (fetchingUser || loadMore).

Error Handling ​

ActivityLogsDialog catches all API and scroll errors silently — neither surfaces a snackbar nor sets an error state visible to the user. The component simply stops loading. If the initial fetch fails, activities remains an empty array and the empty state ("No activity found!") is shown. If a scroll-triggered fetch fails, loadMore is reset to false and the skeleton rows disappear. Pages that integrate the dialog should not attempt to detect or handle errors from within it.

typescript
// Both catch blocks in the component are intentionally empty:
try {
  // API call
} catch {
  // silent fail
}

Activity Interface ​

The Activity interface is defined locally in the component (not in app/types/):

typescript
interface Activity {
  id: number
  created_at: string       // ISO 8601 UTC timestamp
  log_details: string      // human-readable log message
  activity_url?: string | null  // optional deep-link into the DAM
}

formatDate converts created_at to the user's locale:

typescript
// Input:  "2025-07-15T14:23:00Z"
// Output: "Jul 15, 2025 2:23 PM" (en-US locale)
formatDate(dateStr: string): string {
  const date = new Date(dateStr)
  return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
    + ' ' + date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true })
}