1<template>
2 <div
3 class="flex flex-col rounded-5 px-2 py-4 grow w-full h-full overflow-hidden"
4 >
5 <div class="flex items-center gap-2 px-2 text-md-semibold text-ink-gray-8">
6 {{ __("My Recent Activity") }}
7 <Tooltip
8 :text="__('Tickets you\'ve recently worked on or viewed')"
9 side="top"
10 >
11 <LucideInfo class="size-3.5 cursor-pointer text-ink-gray-5" />
12 </Tooltip>
13 </div>
14 <div class="relative mt-5 grow overflow-hidden">
15 <!-- pb matches the fade height below, so the last row never sits under it -->
16 <div class="flex flex-col h-full overflow-auto hide-scrollbar pb-8">
17 <template v-if="!showSkeleton && activities.length > 0">
18 <router-link
19 v-for="activity in activities"
20 :key="activity.name"
21 :to="{
22 name: 'TicketAgent',
23 params: { ticketId: String(activity.name) },
24 }"
25 class="flex items-center gap-3 px-2 py-3 text-sm hover:bg-surface-sidebar rounded-4"
26 >
27 <component
28 :is="iconFor(activity.activity_type)"
29 class="size-4 flex-shrink-0 text-ink-gray-5"
30 />
31 <div class="flex flex-col min-w-0 grow gap-1">
32 <span class="truncate text-ink-gray-8">
33 {{ activity.subject || __("Untitled") }}
34 </span>
35 <span class="truncate text-ink-gray-5">
36 #{{ activity.name }} · {{ label(activity) }}
37 </span>
38 </div>
39 <span class="text-ink-gray-4 whitespace-nowrap tabular-nums">
40 {{ activity.timestamp }}
41 </span>
42 </router-link>
43 </template>
44 <div v-else class="flex flex-col">
45 <div v-for="i in 6" :key="i" class="flex items-center gap-3 p-2 py-3">
46 <div class="size-4 rounded-1 bg-surface-gray-1 flex-shrink-0" />
47 <div class="flex flex-col gap-1 grow">
48 <div class="h-4 w-2/3 rounded-1 bg-surface-gray-1" />
49 <div class="h-3 w-1/3 rounded-1 bg-surface-gray-1" />
50 </div>
51 </div>
52 </div>
53 </div>
54 <EmptyState
55 v-if="!showSkeleton && activities.length === 0"
56 class="absolute inset-0 z-10"
57 :title="__('No recent activity')"
58 :description="__('Tickets you act on will show up here')"
59 variant="overlay"
60 text="md"
61 />
62 <div
63 v-if="!showSkeleton && activities.length > 0"
64 class="pointer-events-none absolute inset-x-0 bottom-0 h-8"
65 :style="{
66 backgroundImage:
67 'linear-gradient(to top, var(--surface-base), transparent)',
68 }"
69 />
70 </div>
71 </div>
72</template>
73
74<script setup lang="ts">
75import EmptyState from "@/components/EmptyState.vue";
76import { __ } from "@/translation";
77import { createResource, Tooltip } from "frappe-ui";
78import { computed, onMounted, ref, type PropType } from "vue";
79import ActivityIcon from "~icons/lucide/activity";
80import ReplyIcon from "~icons/lucide/corner-up-left";
81import EyeIcon from "~icons/lucide/eye";
82import MessageSquareIcon from "~icons/lucide/message-square";
83import UserPlusIcon from "~icons/lucide/user-plus";
84
85interface RecentActivity {
86 name: string;
87 subject: string;
88 activity_type: "replied" | "commented" | "updated" | "viewed" | "assigned";
89 text: string | null;
90 timestamp: string;
91 creation: string;
92}
93
94const props = defineProps({
95 data: {
96 type: Array as PropType<RecentActivity[]>,
97 required: true,
98 },
99});
100
101const typeLabels: Record<string, string> = {
102 replied: __("Replied"),
103 commented: __("Commented"),
104 viewed: __("Viewed"),
105 assigned: __("Assigned"),
106};
107
108const icons = {
109 replied: ReplyIcon,
110 commented: MessageSquareIcon,
111 updated: ActivityIcon,
112 viewed: EyeIcon,
113 assigned: UserPlusIcon,
114};
115
116const hasLoadedOnce = ref(false);
117
118const recentActivityResource = createResource({
119 url: "opendesk.api.agent_home.agent_home.get_recent_activity",
120 onSuccess() {
121 hasLoadedOnce.value = true;
122 },
123});
124
125const activities = computed<RecentActivity[]>(() =>
126 recentActivityResource.fetched ? recentActivityResource.data : props.data
127);
128
129const showSkeleton = computed(
130 () => recentActivityResource.loading && !hasLoadedOnce.value
131);
132
133function iconFor(type: string) {
134 return icons[type as keyof typeof icons] || ActivityIcon;
135}
136
137function label(activity: RecentActivity) {
138 return activity.text || typeLabels[activity.activity_type];
139}
140
141onMounted(() => {
142 if (!Array.isArray(props.data)) {
143 recentActivityResource.fetch();
144 }
145});
146</script>
147