Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: sort #1107

Merged
merged 5 commits into from
Apr 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 8 additions & 20 deletions desk/src/components/desk/tickets/PresetFilters.vue
Original file line number Diff line number Diff line change
@@ -1,34 +1,22 @@
<template>
<Dropdown placement="left" :options="options">
<template #default="{ toggleDropdown }">
<div
class="flex select-none items-center gap-2 rounded-lg border border-gray-300 px-2 py-1"
:class="{ 'cursor-pointer': !$_.isEmpty(options) }"
@click="toggleDropdown"
>
<div class="text-base">
{{ title }}
</div>
<FeatherIcon
v-if="!$_.isEmpty(options)"
name="chevron-down"
class="h-4 w-4 stroke-2"
/>
</div>
</template>
</Dropdown>
<Dropdown
:options="options"
:button="{
label: title,
'icon-right': 'chevron-down',
}"
/>
</template>

<script>
import { ref } from "vue";
import { Dropdown, FeatherIcon } from "frappe-ui";
import { Dropdown } from "frappe-ui";
import { useListFilters } from "@/composables/listFilters";

export default {
name: "PresetFilters",
components: {
Dropdown,
FeatherIcon,
},
setup() {
const listFilters = useListFilters();
Expand Down
6 changes: 2 additions & 4 deletions desk/src/composables/listFilters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,11 +113,9 @@ export function useListFilters() {
}

function queryOrderBy() {
const { sortBy, sortDirection = "desc" } = route.query;

const { sortBy, sortDirection } = route.query;
if (!sortBy) return;

return `${sortBy} ${sortDirection}`;
return [sortBy, sortDirection].join(" ").trim();
}

function applyQuery(query: Array<FilterItem> | string) {
Expand Down
5 changes: 3 additions & 2 deletions desk/src/composables/listManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export function createListManager(options: ListOptions) {
pageLength: pageLength.value,
start: start.value,
cache,
auto: false,
onSuccess() {
meta.submit({
doctype,
Expand Down Expand Up @@ -84,10 +85,10 @@ export function createListManager(options: ListOptions) {

onMounted(() => {
const queryFilters = filterManager.queryFilters();
const sortBy = filterManager.queryOrderBy();
const orderByQuery = filterManager.queryOrderBy();

if (!isEmpty(queryFilters)) filters.value = queryFilters;
if (sortBy) orderBy.value = sortBy;
if (orderByQuery) orderBy.value = orderByQuery;

list.reload();
});
Expand Down
71 changes: 29 additions & 42 deletions desk/src/pages/desk/Tickets.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,41 +16,38 @@
:button="{
label: 'Status',
iconRight: 'chevron-down',
class: 'text-gray-500 bg-gray-200 rounded-lg',
}"
/>
<Dropdown
:options="filterByPriorityOptions"
:button="{
label: 'Priority',
iconRight: 'chevron-down',
class: 'text-gray-500 bg-gray-200 rounded-lg',
}"
/>
</div>
<div class="flex items-center gap-2">
<CompositeFilters />
<Dropdown
:options="sortDropdownOptions"
:options="sortOptions"
:button="{
label: 'Sort',
iconLeft: 'list',
class: 'text-gray-500 bg-gray-200 rounded-lg',
}"
>
<template #default>
<Button>
<template #icon-left>
<IconSort />
<IconSort class="mr-1.5 h-4 w-4" />
</template>
<div class="px-1">Sort</div>
<div>Sort</div>
</Button>
</template>
</Dropdown>
</div>
</div>
<div class="bg-gray-100 px-6 font-sans text-base text-gray-500">
<div class="flex items-center gap-2 px-2 py-1">
<div class="flex items-center gap-2 px-2 py-1.5">
<div class="pl-1 pr-4">
<Input
type="checkbox"
Expand Down Expand Up @@ -111,7 +108,7 @@
<Dropdown :options="priorityDropdownOptions(t.name, t.priority)">
<template #default>
<Badge
:color-map="priorityColorMap"
:color-map="ticketPriorityStore.colorMap"
:label="t.priority"
class="cursor-pointer"
/>
Expand Down Expand Up @@ -245,57 +242,38 @@ export default {
},
inject: ["agents"],
setup() {
const selected = ref(new Set());
const authStore = useAuthStore();
const listFilters = useListFilters();
const ticketStatusStore = useTicketStatusStore();
const selected = ref(new Set());
const showNewTicketDialog = ref(false);
const ticketPriorityStore = useTicketPriorityStore();
const authStore = useAuthStore();
const ticketStatusStore = useTicketStatusStore();

const ticketList = createListManager({
doctype: "HD Ticket",
pageLength: 20,
orderBy: "modified desc",
});

return {
authStore,
listFilters,
selected,
showNewTicketDialog,
ticketList,
ticketPriorityStore,
ticketStatusStore,
};
},
data() {
function sortBy(key, dir) {
this.$router.push({
query: { q: this.$route.query.q, sortBy: key, sortDirection: dir },
});
}

const sortOptions = [
{ label: "HD Ticket Type", value: "ticket_type" },
{ label: "Modified", value: "modified" },
{ label: "Created", value: "created" },
];

const sortDropdownOptions = sortOptions.map((o) => ({
label: o.label,
handler: sortBy.bind(this, o.value, o.sortDirection || "desc"),
}));

return {
priorityColorMap: {
Urgent: "red",
High: "yellow",
Medium: "green",
Low: "blue",
},
showNewTicketDialog: false,
sortDropdownOptions,
};
},
computed: {
sortOptions() {
const options = this.$resources.sortOptions.data || [];
return options.map((o) => ({
label: o,
value: o,
handler: () =>
this.$router.push({ query: { ...this.$route.query, sortBy: o } }),
}));
},
allSelected() {
if (this.$_.isEmpty(this.ticketList.list.data)) return;
return this.ticketList.list.data.length === this.selected.size;
Expand Down Expand Up @@ -417,10 +395,19 @@ export default {
value,
},
];
this.listFilters.applyQuery(this.listFilters.toQuery(f));
this.listFilters.applyQuery(f);
},
},
resources: {
sortOptions() {
return {
url: "helpdesk.extends.doc.sort_options",
auto: true,
params: {
doctype: "HD Ticket",
},
};
},
bulkAssignTicketStatus() {
return {
url: "helpdesk.api.ticket.bulk_assign_ticket_status",
Expand Down
10 changes: 9 additions & 1 deletion desk/src/stores/ticketPriority.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,24 @@ type TicketPriority = {
export const useTicketPriorityStore = defineStore("ticketPriority", () => {
const d__ = createListResource({
doctype: "HD Ticket Priority",
orderBy: "integer_value desc",
auto: true,
});

const options: ComputedRef<Array<TicketPriority>> = computed(
() => d__.list?.data || []
);
const names = computed(() => options.value.map((o) => o.name));
const colorMap = {
Urgent: "red",
High: "yellow",
Medium: "green",
Low: "blue",
};

return {
options,
colorMap,
names,
options,
};
});
5 changes: 3 additions & 2 deletions helpdesk/extends/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from frappe.query_builder import Query
from frappe.query_builder.functions import Count

from .doc import apply_sort
from .qb import get_query


Expand All @@ -30,14 +31,14 @@ def get_list(
table=doctype,
fields=fields,
filters=filters,
order_by=order_by,
offset=start,
limit=limit,
group_by=group_by,
)

query = apply_custom_filters(doctype, query)
query = apply_hook(doctype, query)
query = apply_sort(doctype, order_by, query)

return query.run(as_dict=True, debug=debug)

Expand All @@ -58,12 +59,12 @@ def get_list_meta(
query: Query = get_query(
table=doctype,
filters=filters,
order_by=order_by,
group_by=group_by,
)

query = apply_custom_filters(doctype, query)
query = apply_hook(doctype, query)
query = apply_sort(doctype, order_by, query)

total_count = Count("*").as_("total_count")
query = query.select(total_count)
Expand Down
40 changes: 40 additions & 0 deletions helpdesk/extends/doc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from types import FunctionType

import frappe
from frappe.model.document import get_controller
from frappe.query_builder import Order, Query

SORT_OPTIONS_METHOD = "sort_options"
DEFAULT_SORT_FIELD = "modified"
DEFAULT_SORT_DIRECTION = Order.desc


@frappe.whitelist()
def sort_options(doctype: str):
c = get_controller(doctype)

if not hasattr(c, SORT_OPTIONS_METHOD):
return []

return c.sort_options().keys()


def apply_sort(doctype: str, order_by: str, query: Query):
controller = get_controller(doctype)
fallback = query.orderby(DEFAULT_SORT_FIELD, order=DEFAULT_SORT_DIRECTION)

if not hasattr(controller, SORT_OPTIONS_METHOD):
return fallback

action = controller.sort_options().get(order_by)

if isinstance(action, FunctionType):
return action(query)

if isinstance(action, (list, tuple)):
return query.orderby(action[0], order=action[1])

if isinstance(action, str):
return query.orderby(action, order=DEFAULT_SORT_DIRECTION)

return fallback
31 changes: 29 additions & 2 deletions helpdesk/helpdesk/doctype/hd_ticket/hd_ticket.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import json
from datetime import timedelta
from typing import List
from functools import lru_cache

import frappe
from frappe import _
Expand All @@ -13,7 +14,7 @@
from frappe.email.inbox import link_communication_to_document
from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc
from frappe.query_builder import Case, DocType
from frappe.query_builder import Case, DocType, Order
from frappe.query_builder.functions import Count
from frappe.utils import date_diff, get_datetime, now_datetime, time_diff_in_seconds
from frappe.utils.user import is_website_user
Expand Down Expand Up @@ -89,6 +90,30 @@ def can_ignore_restrictions(user: str) -> bool:
# Must be part of at-least one team which can ignore restrictions
return len(teams) > 0

@staticmethod
@lru_cache
def sort_options():
def by_priority(query: Query, direction: Order):
QBTicket = frappe.qb.DocType("HD Ticket")
QBPriority = frappe.qb.DocType("HD Ticket Priority")

query = (
query.left_join(QBPriority)
.on(QBPriority.name == QBTicket.priority)
.orderby(QBPriority.integer_value, order=direction)
.orderby(QBTicket.resolution_by, order=Order.desc)
)

return query

return {
"Due date": ("resolution_by", Order.asc),
"Created on": ("creation", Order.asc),
"High to low priority": lambda q: by_priority(q, Order.asc),
"Low to high priority": lambda q: by_priority(q, Order.desc),
"Last modified on": "modified",
}

def autoname(self):
return self.name

Expand Down Expand Up @@ -271,7 +296,9 @@ def assign_agent(self, agent):
agent_name = frappe.get_value("HD Agent", agent, "agent_name")
log_ticket_activity(self.name, f"assigned to {agent_name}")
frappe.publish_realtime(
"helpdesk:update-ticket-assignee", {"ticket_id": self.name}, after_commit=True
"helpdesk:update-ticket-assignee",
{"ticket_id": self.name},
after_commit=True,
)

def get_assigned_agent(self):
Expand Down
Loading