Skip to content

Commit

Permalink
fix(ux): response editor (#1220)
Browse files Browse the repository at this point in the history
* fix(ux): response editor

- Add a debounce of `500ms`
- Disable send button conditionally

* fix(ux): ticket single

* chore: remove utils/editor (unused)

* fix: dynamically update ticket id

* feat: add loading indicator

* fix: change focus logic

* fix: scroll
  • Loading branch information
ssiyad authored May 24, 2023
1 parent 1977e1c commit e8b8e14
Show file tree
Hide file tree
Showing 16 changed files with 194 additions and 159 deletions.
3 changes: 2 additions & 1 deletion desk/src/pages/desk/ticket/CommunicationItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
import { toRefs } from "vue";
import dayjs from "dayjs";
import { Avatar, Dropdown, FeatherIcon } from "frappe-ui";
import { editor } from "./data";
import { useTicketStore } from "./data";
import IconDot from "~icons/ph/dot-bold";
import AttachmentItem from "@/components/AttachmentItem.vue";
Expand Down Expand Up @@ -93,6 +93,7 @@ const props = defineProps({
});
const { content, date, sender, senderImage, cc, bcc } = toRefs(props);
const { editor } = useTicketStore();
const dateDisplay = dayjs(date.value).format("h:mm A");
const options = [
{
Expand Down
6 changes: 4 additions & 2 deletions desk/src/pages/desk/ticket/ContactDetails.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<Button
appearance="minimal"
icon="x"
@click="sidebar.isVisible = false"
@click="sidebar.isExpanded = false"
/>
</div>
<div class="flex items-center gap-3 border-b py-6">
Expand Down Expand Up @@ -51,11 +51,13 @@
import { isEmpty } from "lodash";
import { computed } from "vue";
import { Avatar, Button, createDocumentResource } from "frappe-ui";
import { sidebar, ticket } from "./data";
import { useTicketStore } from "./data";
import CustomFieldList from "./CustomFieldList.vue";
import OpenTicketList from "./OpenTicketList.vue";
import IconEmail from "~icons/espresso/email";
const { sidebar, ticket } = useTicketStore();
const c = createDocumentResource({
doctype: "Contact",
name: ticket.doc.contact,
Expand Down
97 changes: 56 additions & 41 deletions desk/src/pages/desk/ticket/ConversationBox.vue
Original file line number Diff line number Diff line change
@@ -1,68 +1,82 @@
<template>
<div ref="listElement" class="flex flex-col items-center overflow-scroll">
<div class="content flex flex-col gap-4">
<div v-for="(c, i) in conversations" :key="c.name" class="mt-4">
<div v-if="isNewDay(i)">
<div class="flex items-center">
<div class="bg h-0.5 grow rounded-full bg-gray-100"></div>
<div class="my-2 ml-5 grow-0 text-sm text-gray-800">
{{ dayShort(c.creation) }}
<div class="flex flex-col overflow-hidden">
<div
v-if="isLoaded"
ref="listElement"
class="flex w-full flex-col items-center gap-4 overflow-scroll"
>
<div class="content">
<div v-for="(c, i) in conversations" :key="c.name" class="mt-4">
<div v-if="isNewDay(i)">
<div class="my-4 border-t text-center">
<div class="-translate-y-1/2">
<span class="bg-white px-2 text-xs text-gray-700">
{{ dayShort(c.creation) }}
</span>
</div>
</div>
</div>
<CommunicationItem
v-if="c.isCommunication"
:content="c.content"
:date="c.creation"
:sender="c.sender.full_name"
:sender-image="c.sender.image"
:cc="c.cc"
:bcc="c.bcc"
:attachments="c.attachments"
/>
<CommentItem
v-else
:name="c.name"
:content="c.content"
:date="c.creation"
:sender="c.sender"
/>
</div>
<CommunicationItem
v-if="c.isCommunication"
:content="c.content"
:date="c.creation"
:sender="c.sender.full_name"
:sender-image="c.sender.image"
:cc="c.cc"
:bcc="c.bcc"
:attachments="c.attachments"
/>
<CommentItem
v-else
:name="c.name"
:content="c.content"
:date="c.creation"
:sender="c.sender"
/>
</div>
</div>
<div v-else class="flex grow items-center justify-center">
<LoadingIndicator class="w-5 text-gray-900" />
</div>
</div>
</template>

<script setup lang="ts">
import { computed, onUnmounted, ref, watch } from "vue";
import { useScroll } from "@vueuse/core";
import { debounce, LoadingIndicator } from "frappe-ui";
import dayjs from "dayjs";
import { orderBy, unionBy } from "lodash";
import { socket } from "@/socket";
import { ticket } from "./data";
import { useTicketStore } from "./data";
import CommentItem from "./CommentItem.vue";
import CommunicationItem from "./CommunicationItem.vue";
type SocketData = {
ticket_id: string;
};
ticket.getCommunications
.submit()
.then(() => (isCommunicationsLoaded.value = true));
ticket.getComments.submit().then(() => (isCommentsLoaded.value = true));
const listElement = ref<HTMLElement | null>(null);
const { y: scrollY } = useScroll(listElement, { behavior: "smooth" });
const { editor, ticket } = useTicketStore();
const listElement = ref(null);
const isCommunicationsLoaded = ref(false);
const isCommentsLoaded = ref(false);
const isLoaded = computed(
() => isCommunicationsLoaded.value && isCommentsLoaded.value
);
watch(isLoaded, (v) => {
if (v) scrollToBottom();
if (v) scrollBottom();
});
watch(
() => editor.isExpanded,
() => scrollBottom()
);
ticket.getCommunications
.submit()
.then(() => (isCommunicationsLoaded.value = true));
ticket.getComments.submit().then(() => (isCommentsLoaded.value = true));
const ticketId = computed(() => ticket.doc.name);
const communications = computed(
Expand All @@ -75,17 +89,18 @@ const conversations = computed(() =>
)
);
const scrollBottom = debounce(() => {
const { y } = useScroll(listElement, { behavior: "smooth" });
y.value = listElement.value.scrollHeight;
}, 500);
function mapCommunication(c) {
return {
...c,
isCommunication: true,
};
}
function scrollToBottom() {
scrollY.value = listElement.value.scrollHeight;
}
function isNewDay(index: number) {
if (index === 0) return true;
Expand All @@ -108,12 +123,12 @@ function dayShort(date: string) {
socket.on("helpdesk:new-communication", (data: SocketData) => {
if (data.ticket_id !== ticketId.value) return;
ticket.getCommunications.reload().then(() => scrollToBottom());
ticket.getCommunications.reload().then(() => scrollBottom());
});
socket.on("helpdesk:new-ticket-comment", (data: SocketData) => {
if (data.ticket_id !== ticketId.value) return;
ticket.getComments.reload().then(() => scrollToBottom());
ticket.getComments.reload().then(() => scrollBottom());
});
socket.on("helpdesk:delete-ticket-comment", (data: SocketData) => {
Expand Down
3 changes: 2 additions & 1 deletion desk/src/pages/desk/ticket/CustomFieldList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@
<script setup lang="ts">
import { isEmpty } from "lodash";
import { computed } from "vue";
import { ticket } from "./data";
import { useTicketStore } from "./data";
import IconTeams from "~icons/espresso/teams";
import IconWebLink from "~icons/espresso/web-link";
const { ticket } = useTicketStore();
const fields = computed(() => ticket.doc?.custom_fields);
</script>
17 changes: 7 additions & 10 deletions desk/src/pages/desk/ticket/OpenTicketList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,12 @@
<IconCaretDown v-else class="h-4 w-4 text-gray-600" />
</div>
<div v-if="isExpanded" class="flex flex-col gap-2 pt-4">
<div v-for="ticket in tickets" :key="ticket.name">
<router-link
:to="ticket.to"
target="_blank"
class="flex items-start gap-2"
>
<div v-for="t in tickets" :key="t.name">
<router-link :to="t.to" target="_blank" class="flex items-start gap-2">
<div class="flex h-5 w-5 items-center justify-center">
<IconWebLink class="h-5 w-5 text-gray-600" />
</div>
<div class="text-base text-gray-800">{{ ticket.subject }}</div>
<div class="text-base text-gray-800">{{ t.subject }}</div>
</router-link>
</div>
</div>
Expand All @@ -36,7 +32,7 @@ import { isEmpty } from "lodash";
import { computed, ref } from "vue";
import { createListResource } from "frappe-ui";
import { AGENT_PORTAL_TICKET } from "@/router";
import { ticket } from "./data";
import { useTicketStore } from "./data";
import IconWebLink from "~icons/espresso/web-link";
import IconCaretDown from "~icons/ph/caret-down";
import IconCaretUp from "~icons/ph/caret-up";
Expand All @@ -54,9 +50,10 @@ class Ticket {
}
}
const { ticket } = useTicketStore();
const isExpanded = ref(false);
const t = createListResource({
const ticketRes = createListResource({
doctype: "HD Ticket",
fields: ["name", "subject"],
filters: {
Expand All @@ -68,6 +65,6 @@ const t = createListResource({
});
const tickets = computed(
() => t.data?.map((t: Ticket) => new Ticket(t.name, t.subject)) || []
() => ticketRes.data?.map((t: Ticket) => new Ticket(t.name, t.subject)) || []
);
</script>
11 changes: 6 additions & 5 deletions desk/src/pages/desk/ticket/SideBar.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<div class="flex">
<TabGroup vertical>
<TabPanels v-if="sidebar.isVisible" class="main-panel h-full">
<TabPanels v-if="sidebar.isExpanded" class="main-panel h-full">
<TabPanel v-for="item in items" :key="item.name" class="h-full">
<component :is="item.component" class="h-full" />
</TabPanel>
Expand All @@ -11,10 +11,10 @@
<div
class="flex h-7 w-7 items-center justify-center rounded-lg text-gray-600"
:class="{
'bg-gray-200': sidebar.isVisible && selected,
'text-gray-900': sidebar.isVisible && selected,
'bg-gray-200': sidebar.isExpanded && selected,
'text-gray-900': sidebar.isExpanded && selected,
}"
@click="sidebar.isVisible = true"
@click="sidebar.isExpanded = true"
>
<component :is="item.icon" />
</div>
Expand All @@ -26,14 +26,15 @@

<script setup lang="ts">
import { TabGroup, TabList, Tab, TabPanels, TabPanel } from "@headlessui/vue";
import { sidebar } from "./data";
import { useTicketStore } from "./data";
import ContactDetails from "./ContactDetails.vue";
import TicketDetails from "./TicketDetails.vue";
import TicketHistory from "./TicketHistory.vue";
import IconActivity from "~icons/espresso/activity";
import IconAlert from "~icons/espresso/alert-circle";
import IconDetails from "~icons/espresso/details";
const { sidebar } = useTicketStore();
const items = [
{
name: "Ticket Details",
Expand Down
5 changes: 3 additions & 2 deletions desk/src/pages/desk/ticket/TicketDetails.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<Button
appearance="minimal"
icon="x"
@click="sidebar.isVisible = false"
@click="sidebar.isExpanded = false"
/>
</div>
<div class="my-6 flex flex-col justify-between gap-3.5">
Expand Down Expand Up @@ -113,14 +113,15 @@ import { useTicketPriorityStore } from "@/stores/ticketPriority";
import { useTicketStatusStore } from "@/stores/ticketStatus";
import { useTicketTypeStore } from "@/stores/ticketType";
import { createToast } from "@/utils/toasts";
import { sidebar, ticket } from "./data";
import { useTicketStore } from "./data";
const agentStore = useAgentStore();
const keymapStore = useKeymapStore();
const teamStore = useTeamStore();
const ticketPriorityStore = useTicketPriorityStore();
const ticketStatusStore = useTicketStatusStore();
const ticketTypeStore = useTicketTypeStore();
const { sidebar, ticket } = useTicketStore();
const isSaveButtonVisible = ref(false);
Expand Down
6 changes: 4 additions & 2 deletions desk/src/pages/desk/ticket/TicketHistory.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<Button
appearance="minimal"
icon="x"
@click="sidebar.isVisible = false"
@click="sidebar.isExpanded = false"
/>
</div>
<div class="overflow-scroll px-4">
Expand Down Expand Up @@ -34,7 +34,7 @@
import { computed, ComputedRef } from "vue";
import { Button, createListResource, Tooltip } from "frappe-ui";
import dayjs from "dayjs";
import { sidebar, ticket } from "./data";
import { useTicketStore } from "./data";
import IconDot from "~icons/ph/dot-bold";
class Activity {
Expand All @@ -59,6 +59,8 @@ class Activity {
}
}
const { sidebar, ticket } = useTicketStore();
const r = createListResource({
doctype: "HD Ticket Activity",
fields: ["name", "creation", "action", "owner"],
Expand Down
6 changes: 3 additions & 3 deletions desk/src/pages/desk/ticket/TicketSingle.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
<TopBar />
<div class="flex grow overflow-hidden">
<div class="flex grow flex-col">
<ConversationBox />
<div class="grow"></div>
<ConversationBox class="grow" />
<ResponseEditor />
</div>
<SideBar />
Expand All @@ -15,7 +14,7 @@
<script setup lang="ts">
import { ref, onUnmounted } from "vue";
import { useConfigStore } from "@/stores/config";
import { deinit, init, ticket } from "./data";
import { useTicketStore } from "./data";
import ConversationBox from "./ConversationBox.vue";
import ResponseEditor from "./editor/ResponseEditor.vue";
import SideBar from "./SideBar.vue";
Expand All @@ -27,6 +26,7 @@ const props = defineProps({
required: true,
},
});
const { init, deinit, ticket } = useTicketStore();
const isResLoaded = ref(false);
const configStore = useConfigStore();
Expand Down
3 changes: 2 additions & 1 deletion desk/src/pages/desk/ticket/TopBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import { Tooltip } from "frappe-ui";
import { useClipboard } from "@vueuse/core";
import dayjs from "dayjs";
import { AGENT_PORTAL_TICKET_LIST } from "@/router";
import { ticket } from "./data";
import { useTicketStore } from "./data";
import { createToast } from "@/utils/toasts";
import IconAtSign from "~icons/espresso/at-sign";
import IconCaretLeft from "~icons/ph/caret-left";
Expand All @@ -52,6 +52,7 @@ import IconWeb from "~icons/espresso/web";
const { copy } = useClipboard();
const router = useRouter();
const { ticket } = useTicketStore();
const date = computed(() => dayjs(ticket.doc.modified).tz(dayjs.tz.guess()));
const dateShort = computed(() => date.value.fromNow());
Expand Down
Loading

0 comments on commit e8b8e14

Please sign in to comment.