diff --git a/desk/src/components/knowledge-base/CategoryFolder.vue b/desk/src/components/knowledge-base/CategoryFolder.vue
new file mode 100644
index 000000000..0d4a5b077
--- /dev/null
+++ b/desk/src/components/knowledge-base/CategoryFolder.vue
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+ {{ category.category_name }}
+
+
+ {{ dayjs.tz(category.modified).fromNow() }}
+
+
+
+
+
+
+
+
+
diff --git a/desk/src/components/knowledge-base/CategoryFolderContainer.vue b/desk/src/components/knowledge-base/CategoryFolderContainer.vue
new file mode 100644
index 000000000..8346d0d5a
--- /dev/null
+++ b/desk/src/components/knowledge-base/CategoryFolderContainer.vue
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
diff --git a/desk/src/components/knowledge-base/CategoryModal.vue b/desk/src/components/knowledge-base/CategoryModal.vue
new file mode 100644
index 000000000..b9088b4e6
--- /dev/null
+++ b/desk/src/components/knowledge-base/CategoryModal.vue
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
diff --git a/desk/src/components/knowledge-base/MoveToCategoryModal.vue b/desk/src/components/knowledge-base/MoveToCategoryModal.vue
new file mode 100644
index 000000000..d0aa2b8b3
--- /dev/null
+++ b/desk/src/components/knowledge-base/MoveToCategoryModal.vue
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
diff --git a/desk/src/components/layouts/layoutSettings.ts b/desk/src/components/layouts/layoutSettings.ts
index fa831de67..ff927a243 100644
--- a/desk/src/components/layouts/layoutSettings.ts
+++ b/desk/src/components/layouts/layoutSettings.ts
@@ -11,6 +11,7 @@ import {
AGENT_PORTAL_CUSTOMER_LIST,
AGENT_PORTAL_TEAM_LIST,
AGENT_PORTAL_TICKET_LIST,
+ AGENT_PORTAL_KNOWLEDGE_BASE,
} from "@/router";
export const agentPortalSidebarOptions = [
@@ -27,7 +28,7 @@ export const agentPortalSidebarOptions = [
{
label: "Knowledge base",
icon: LucideBookOpen,
- to: "DeskKBHome",
+ to: "AgentKnowledgeBase",
},
{
label: "Teams",
@@ -60,6 +61,6 @@ export const customerPortalSidebarOptions = [
{
label: "Knowledge base",
icon: LucideBookOpen,
- to: "KnowledgeBasePublicNew",
+ to: "CustomerKnowledgeBase",
},
];
diff --git a/desk/src/components/view-controls/index.ts b/desk/src/components/view-controls/index.ts
index 14149aed3..dba10617e 100644
--- a/desk/src/components/view-controls/index.ts
+++ b/desk/src/components/view-controls/index.ts
@@ -1,3 +1,4 @@
export { default as Filter } from "./Filter.vue";
export { default as SortBy } from "./SortBy.vue";
export { default as QuickFilters } from "./QuickFilters.vue";
+export { default as Reload } from "./Reload.vue";
diff --git a/desk/src/composables/columns.ts b/desk/src/composables/columns.ts
deleted file mode 100644
index ff777033f..000000000
--- a/desk/src/composables/columns.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import { useRoute } from "vue-router";
-import { useStorage } from "@vueuse/core";
-
-/**
- * @param doctype - The DocType to use
- */
-export function useColumns(doctype: string) {
- const route = useRoute();
- const prefix = "hide_columns";
- const storageKey = [prefix, route.path, doctype].join("_");
- const storage = useStorage(storageKey, new Set());
-
- /**
- * @param key - The column key to toggle
- * @returns void
- * @description Toggles the column visibility
- */
- function toggle(key: string) {
- if (!storage.value.delete(key)) {
- storage.value.add(key);
- }
- }
-
- return {
- storage,
- toggle,
- };
-}
diff --git a/desk/src/pages/HRoot.vue b/desk/src/pages/HRoot.vue
index 925c7df85..1d0ff81a0 100644
--- a/desk/src/pages/HRoot.vue
+++ b/desk/src/pages/HRoot.vue
@@ -2,11 +2,7 @@
import { useRouter } from "vue-router";
import { useAuthStore } from "@/stores/auth";
import { useConfigStore } from "@/stores/config";
-import {
- AGENT_PORTAL_LANDING,
- CUSTOMER_PORTAL_LANDING,
- KB_PUBLIC,
-} from "@/router";
+import { AGENT_PORTAL_LANDING, CUSTOMER_PORTAL_LANDING } from "@/router";
const router = useRouter();
const authStore = useAuthStore();
diff --git a/desk/src/pages/KnowledgeBaseArticle.vue b/desk/src/pages/KnowledgeBaseArticle.vue
index 5b808bef4..2bf1bc12d 100644
--- a/desk/src/pages/KnowledgeBaseArticle.vue
+++ b/desk/src/pages/KnowledgeBaseArticle.vue
@@ -110,7 +110,7 @@ import {
AGENT_PORTAL_KNOWLEDGE_BASE_SUB_CATEGORY,
CUSTOMER_PORTAL_NEW_TICKET,
} from "@/router";
-import { createToast } from "@/utils";
+import { createToast, textEditorMenuButtons } from "@/utils";
import { useAuthStore } from "@/stores/auth";
import { useError } from "@/composables/error";
import { LayoutHeader } from "@/components";
@@ -353,7 +353,9 @@ const insertRes = createResource({
},
});
},
- onError: useError({ title: "Error creating article" }),
+ onError(err){
+ useError(err);,
+ }
});
const setValueRes = createResource({
@@ -411,44 +413,6 @@ const backTo = computed(() => ({
},
}));
-const textEditorMenuButtons = [
- "Paragraph",
- ["Heading 2", "Heading 3", "Heading 4", "Heading 5", "Heading 6"],
- "Separator",
- "Bold",
- "Italic",
- "Separator",
- "Bullet List",
- "Numbered List",
- "Separator",
- "Align Left",
- "Align Center",
- "Align Right",
- "FontColor",
- "Separator",
- "Image",
- "Video",
- "Link",
- "Blockquote",
- "Code",
- "Horizontal Rule",
- [
- "InsertTable",
- "AddColumnBefore",
- "AddColumnAfter",
- "DeleteColumn",
- "AddRowBefore",
- "AddRowAfter",
- "DeleteRow",
- "MergeCells",
- "SplitCell",
- "ToggleHeaderColumn",
- "ToggleHeaderRow",
- "ToggleHeaderCell",
- "DeleteTable",
- ],
-];
-
const textEditorContentWithIDs = computed(() =>
article.data?.content ? addLinksToHeadings(article.data?.content) : null
);
diff --git a/desk/src/pages/knowledge-base-v2/KnowledgeBasePublic.vue b/desk/src/pages/knowledge-base-v2/KnowledgeBasePublic.vue
deleted file mode 100644
index 9c02b4415..000000000
--- a/desk/src/pages/knowledge-base-v2/KnowledgeBasePublic.vue
+++ /dev/null
@@ -1,81 +0,0 @@
-
-
-
-
-
-
-
diff --git a/desk/src/pages/knowledge-base/Article.vue b/desk/src/pages/knowledge-base/Article.vue
new file mode 100644
index 000000000..6901dc14d
--- /dev/null
+++ b/desk/src/pages/knowledge-base/Article.vue
@@ -0,0 +1,370 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ user.full_name || user.name }}
+
+
+
+
+ {{ dayjs(article.data.modified).short() }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{
+ content = event;
+ }"
+ placeholder="Write your article here..."
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/desk/src/pages/knowledge-base/Articles.vue b/desk/src/pages/knowledge-base/Articles.vue
new file mode 100644
index 000000000..97c3c4f2b
--- /dev/null
+++ b/desk/src/pages/knowledge-base/Articles.vue
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
diff --git a/desk/src/pages/knowledge-base/KnowledgeBase.vue b/desk/src/pages/knowledge-base/KnowledgeBase.vue
deleted file mode 100644
index 1030e6492..000000000
--- a/desk/src/pages/knowledge-base/KnowledgeBase.vue
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
- Knowledge base
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/desk/src/pages/knowledge-base/KnowledgeBaseAgent.vue b/desk/src/pages/knowledge-base/KnowledgeBaseAgent.vue
new file mode 100644
index 000000000..5a98fef0e
--- /dev/null
+++ b/desk/src/pages/knowledge-base/KnowledgeBaseAgent.vue
@@ -0,0 +1,346 @@
+
+
+
+
+ Knowledge base
+
+
+
+
+
+
+
+
$router.push(`kb/articles/${row}`)"
+ />
+
+
+
+
+
+
diff --git a/desk/src/pages/knowledge-base/KnowledgeBaseArticleActionsEdit.vue b/desk/src/pages/knowledge-base/KnowledgeBaseArticleActionsEdit.vue
deleted file mode 100644
index f2715403d..000000000
--- a/desk/src/pages/knowledge-base/KnowledgeBaseArticleActionsEdit.vue
+++ /dev/null
@@ -1,38 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/desk/src/pages/knowledge-base/KnowledgeBaseArticleActionsNew.vue b/desk/src/pages/knowledge-base/KnowledgeBaseArticleActionsNew.vue
deleted file mode 100644
index bca4b3dbe..000000000
--- a/desk/src/pages/knowledge-base/KnowledgeBaseArticleActionsNew.vue
+++ /dev/null
@@ -1,37 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/desk/src/pages/knowledge-base/KnowledgeBaseArticleActionsView.vue b/desk/src/pages/knowledge-base/KnowledgeBaseArticleActionsView.vue
deleted file mode 100644
index 7d8f8b78c..000000000
--- a/desk/src/pages/knowledge-base/KnowledgeBaseArticleActionsView.vue
+++ /dev/null
@@ -1,72 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
diff --git a/desk/src/pages/knowledge-base/KnowledgeBaseArticleTopEdit.vue b/desk/src/pages/knowledge-base/KnowledgeBaseArticleTopEdit.vue
deleted file mode 100644
index 50baa61d5..000000000
--- a/desk/src/pages/knowledge-base/KnowledgeBaseArticleTopEdit.vue
+++ /dev/null
@@ -1,52 +0,0 @@
-
-
-
-
-
- {{ authorFullname }}
-
-
-
-
Created
-
- {{ dayjs.tz(creation).fromNow() }}
-
-
|
-
Modified
-
- {{ dayjs.tz(modified).fromNow() }}
-
-
-
-
-
-
-
-
-
diff --git a/desk/src/pages/knowledge-base/KnowledgeBaseArticleTopNew.vue b/desk/src/pages/knowledge-base/KnowledgeBaseArticleTopNew.vue
deleted file mode 100644
index 7f81167af..000000000
--- a/desk/src/pages/knowledge-base/KnowledgeBaseArticleTopNew.vue
+++ /dev/null
@@ -1,34 +0,0 @@
-
-
-
-
-
- {{ authorFullname }}
-
-
-
-
-
-
-
-
-
diff --git a/desk/src/pages/knowledge-base/KnowledgeBaseArticleTopPublic.vue b/desk/src/pages/knowledge-base/KnowledgeBaseArticleTopPublic.vue
deleted file mode 100644
index 4f6adc018..000000000
--- a/desk/src/pages/knowledge-base/KnowledgeBaseArticleTopPublic.vue
+++ /dev/null
@@ -1,53 +0,0 @@
-
-
-
-
-
-
-
- {{ subCategoryName }}
-
-
-
-
-
-
diff --git a/desk/src/pages/knowledge-base/KnowledgeBaseArticleTopView.vue b/desk/src/pages/knowledge-base/KnowledgeBaseArticleTopView.vue
deleted file mode 100644
index 3e32847b8..000000000
--- a/desk/src/pages/knowledge-base/KnowledgeBaseArticleTopView.vue
+++ /dev/null
@@ -1,83 +0,0 @@
-
-
-
-
-
-
- {{ authorFullname }}
-
-
-
-
- {{ dayjs(creation).short() }}
-
-
-
-
- {{ status }}
-
-
-
-
-
-
-
-
diff --git a/desk/src/pages/knowledge-base/KnowledgeBaseCategory.vue b/desk/src/pages/knowledge-base/KnowledgeBaseCategory.vue
deleted file mode 100644
index 025a2140c..000000000
--- a/desk/src/pages/knowledge-base/KnowledgeBaseCategory.vue
+++ /dev/null
@@ -1,225 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ c.article_count + " articles" }}
-
-
-
-
-
-
-
-
-
diff --git a/desk/src/pages/knowledge-base/KnowledgeBaseCategoryHeader.vue b/desk/src/pages/knowledge-base/KnowledgeBaseCategoryHeader.vue
deleted file mode 100644
index 870892f78..000000000
--- a/desk/src/pages/knowledge-base/KnowledgeBaseCategoryHeader.vue
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-
-
- {{ description }}
-
-
-
-
-
diff --git a/desk/src/pages/knowledge-base/KnowledgeBaseCategoryNew.vue b/desk/src/pages/knowledge-base/KnowledgeBaseCategoryNew.vue
deleted file mode 100644
index 8ca8bcb67..000000000
--- a/desk/src/pages/knowledge-base/KnowledgeBaseCategoryNew.vue
+++ /dev/null
@@ -1,81 +0,0 @@
-
-
-
-
-
diff --git a/desk/src/pages/knowledge-base/KnowledgeBaseCustomer.vue b/desk/src/pages/knowledge-base/KnowledgeBaseCustomer.vue
new file mode 100644
index 000000000..460426980
--- /dev/null
+++ b/desk/src/pages/knowledge-base/KnowledgeBaseCustomer.vue
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
diff --git a/desk/src/pages/knowledge-base/KnowledgeBaseIconSelector.vue b/desk/src/pages/knowledge-base/KnowledgeBaseIconSelector.vue
deleted file mode 100644
index f6e354d02..000000000
--- a/desk/src/pages/knowledge-base/KnowledgeBaseIconSelector.vue
+++ /dev/null
@@ -1,47 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/desk/src/pages/knowledge-base/KnowledgeBaseSidebar.vue b/desk/src/pages/knowledge-base/KnowledgeBaseSidebar.vue
deleted file mode 100644
index 7875fd72c..000000000
--- a/desk/src/pages/knowledge-base/KnowledgeBaseSidebar.vue
+++ /dev/null
@@ -1,78 +0,0 @@
-
-
-
-
-
diff --git a/desk/src/pages/knowledge-base/KnowledgeBaseSubcategory.vue b/desk/src/pages/knowledge-base/KnowledgeBaseSubcategory.vue
deleted file mode 100644
index 34f900972..000000000
--- a/desk/src/pages/knowledge-base/KnowledgeBaseSubcategory.vue
+++ /dev/null
@@ -1,177 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/desk/src/pages/knowledge-base/NewArticle.vue b/desk/src/pages/knowledge-base/NewArticle.vue
new file mode 100644
index 000000000..c2ade57bb
--- /dev/null
+++ b/desk/src/pages/knowledge-base/NewArticle.vue
@@ -0,0 +1,179 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ in
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/desk/src/pages/ticket/TicketNewArticles.vue b/desk/src/pages/ticket/TicketNewArticles.vue
index 25d2eda2d..9e1b7c6fc 100644
--- a/desk/src/pages/ticket/TicketNewArticles.vue
+++ b/desk/src/pages/ticket/TicketNewArticles.vue
@@ -8,7 +8,7 @@
@@ -24,7 +24,7 @@
- import("@/pages/knowledge-base-v2/KnowledgeBasePublic.vue"),
- },
- {
- path: "articles/:articleId?",
- name: "KBArticlePublicNew",
- component: () => import("@/pages/KnowledgeBaseArticle.vue"),
- props: true,
- },
- ],
+ path: "kb-public",
+ name: "CustomerKnowledgeBase",
+ component: () =>
+ import("@/pages/knowledge-base/KnowledgeBaseCustomer.vue"),
+ },
+ {
+ path: "kb-public/:categoryId",
+ name: "Articles",
+ component: () => import("@/pages/knowledge-base/Articles.vue"),
+ props: true,
+ },
+ {
+ path: "kb-public/articles/:articleId",
+ name: "ArticlePublic",
+ component: () => import("@/pages/knowledge-base/Article.vue"),
+ props: true,
},
],
},
@@ -150,30 +149,20 @@ const routes = [
},
{
path: "kb",
- name: AGENT_PORTAL_KNOWLEDGE_BASE,
- component: () => import("@/pages/knowledge-base/KnowledgeBase.vue"),
- children: [
- {
- path: ":categoryId",
- name: AGENT_PORTAL_KNOWLEDGE_BASE_CATEGORY,
- props: true,
- component: () =>
- import("@/pages/knowledge-base/KnowledgeBaseCategory.vue"),
- },
- {
- path: ":categoryId/:subCategoryId",
- name: AGENT_PORTAL_KNOWLEDGE_BASE_SUB_CATEGORY,
- props: true,
- component: () =>
- import("@/pages/knowledge-base/KnowledgeBaseSubcategory.vue"),
- },
- ],
+ name: "AgentKnowledgeBase",
+ component: () =>
+ import("@/pages/knowledge-base/KnowledgeBaseAgent.vue"),
},
{
path: "kb/articles/:articleId",
- name: AGENT_PORTAL_KNOWLEDGE_BASE_ARTICLE,
+ name: "Article",
+ component: () => import("@/pages/knowledge-base/Article.vue"),
props: true,
- component: () => import("@/pages/KnowledgeBaseArticle.vue"),
+ },
+ {
+ path: "articles/new",
+ name: "NewArticle",
+ component: () => import("@/pages/knowledge-base/NewArticle.vue"),
},
{
path: "customers",
diff --git a/desk/src/stores/knowledgeBase.ts b/desk/src/stores/knowledgeBase.ts
new file mode 100644
index 000000000..d10ccd1a6
--- /dev/null
+++ b/desk/src/stores/knowledgeBase.ts
@@ -0,0 +1,86 @@
+import { createResource } from "frappe-ui";
+
+// Title
+export const newArticle = createResource({
+ url: "frappe.client.insert",
+ makeParams({ title, content, category }) {
+ return {
+ doc: {
+ doctype: "HD Article",
+ title,
+ content,
+ category,
+ },
+ };
+ },
+ validate({ doc }) {
+ if (!doc.title) throw "Title is required";
+ if (!doc.content) throw "Content is required";
+ },
+});
+
+export const updateRes = createResource({
+ url: "frappe.client.set_value",
+});
+
+export const deleteRes = createResource({
+ url: "frappe.client.delete",
+});
+
+export const deleteArticles = createResource({
+ url: "helpdesk.api.knowledge_base.delete_articles",
+ makeParams({ articles }) {
+ return {
+ articles,
+ };
+ },
+ validate({ articles }) {
+ if (!articles) throw "Articles are required";
+ },
+});
+
+// Category
+export const newCategory = createResource({
+ url: "helpdesk.api.knowledge_base.create_category",
+ makeParams({ title }) {
+ return {
+ title,
+ };
+ },
+ validate(title: string) {
+ if (!title) throw "Title is required";
+ },
+});
+
+export const updateCategoryTitle = createResource({
+ url: "frappe.client.set_value",
+ validate({ name, value }) {
+ if (!value) throw "Title is required";
+ },
+});
+
+export const deleteCategory = createResource({
+ url: "helpdesk.api.knowledge_base.delete_category",
+ makeParams({ name }) {
+ return {
+ name,
+ };
+ },
+ validate({ name }) {
+ if (!name) throw "Category is required";
+ },
+});
+
+export const moveToCategory = createResource({
+ url: "helpdesk.api.knowledge_base.move_to_category",
+ makeParams({ category, articles }) {
+ return {
+ category,
+ articles,
+ };
+ },
+ validate({ category, articles }) {
+ if (!category) throw "Category is required";
+ if (!articles) throw "Articles are required";
+ },
+});
diff --git a/desk/src/types.ts b/desk/src/types.ts
index 79e031d72..b6b288095 100644
--- a/desk/src/types.ts
+++ b/desk/src/types.ts
@@ -1,9 +1,9 @@
import { Component } from "vue";
-export interface Resource {
+export interface Resource {
auto: boolean;
loading: boolean;
- data: A;
+ data: T;
pageLength: number;
totalCount: number;
hasNextPage: boolean;
@@ -216,12 +216,17 @@ export interface RootCategory {
export interface Article {
name: string;
title: string;
- category: string;
+ category_name: string;
+ category_id: string;
published_on: string;
- author: string;
+ author: Author;
subtitle: string;
article_image: string | null;
_user_tags: string | null;
+ status: string;
+ creation: string;
+ content: string;
+ modified: string;
}
export interface SubCategory {
@@ -234,7 +239,7 @@ export interface SubCategory {
export interface Author {
name: string;
image: string | null;
- email?: string;
+ email: string;
}
export interface Category {
@@ -254,3 +259,8 @@ export interface BadgeStatus {
}
export type FeedbackAction = 0 | 1 | 2; // 0: neutral, 1: like, 2: dislike
+
+export interface View {
+ view_type: string;
+ group_by_field: string;
+}
diff --git a/desk/src/utils.ts b/desk/src/utils.ts
index 9241aa645..9560501ab 100644
--- a/desk/src/utils.ts
+++ b/desk/src/utils.ts
@@ -1,5 +1,6 @@
import { useClipboard, useDateFormat, useTimeAgo } from "@vueuse/core";
import { toast } from "frappe-ui";
+import { RouteLocation } from "vue-router";
import zod from "zod";
/**
* Wrapper to create toasts, supplied with default options.
@@ -116,3 +117,44 @@ export function setupCustomActions(data, obj) {
data._customActions = actions;
}
+
+export const isCustomerPortal = (route: RouteLocation) =>
+ route.meta.public ?? false;
+
+export const textEditorMenuButtons = [
+ "Paragraph",
+ ["Heading 2", "Heading 3", "Heading 4", "Heading 5", "Heading 6"],
+ "Separator",
+ "Bold",
+ "Italic",
+ "Separator",
+ "Bullet List",
+ "Numbered List",
+ "Separator",
+ "Align Left",
+ "Align Center",
+ "Align Right",
+ "FontColor",
+ "Separator",
+ "Image",
+ "Video",
+ "Link",
+ "Blockquote",
+ "Code",
+ "Horizontal Rule",
+ [
+ "InsertTable",
+ "AddColumnBefore",
+ "AddColumnAfter",
+ "DeleteColumn",
+ "AddRowBefore",
+ "AddRowAfter",
+ "DeleteRow",
+ "MergeCells",
+ "SplitCell",
+ "ToggleHeaderColumn",
+ "ToggleHeaderRow",
+ "ToggleHeaderCell",
+ "DeleteTable",
+ ],
+];
diff --git a/frappe-ui b/frappe-ui
index c04c9feaa..c2ff9c9d4 160000
--- a/frappe-ui
+++ b/frappe-ui
@@ -1 +1 @@
-Subproject commit c04c9feaa0e009e754903666d2c6bfe12328b96e
+Subproject commit c2ff9c9d49cdbbfcaea3cf96c72adecf0bbe1e14
diff --git a/helpdesk/api/doc.py b/helpdesk/api/doc.py
index 96313de50..cf366d1e5 100644
--- a/helpdesk/api/doc.py
+++ b/helpdesk/api/doc.py
@@ -125,9 +125,13 @@ def get_list_data(
columns=None,
rows=None,
show_customer_portal_fields=False,
+ view=None,
):
is_default = True
+ view_type = view.get("view_type") if view else None
+ group_by_field = view.get("group_by_field") if view else None
+
if columns or rows:
is_default = False
columns = frappe.parse_json(columns)
@@ -153,17 +157,17 @@ def get_list_data(
# rows = frappe.parse_json(list_view_settings.rows)
# is_default = False
# else:
- list = get_controller(doctype)
+ _list = get_controller(doctype)
# flake8: noqa
if is_default:
- if hasattr(list, "default_list_data"):
+ if hasattr(_list, "default_list_data"):
columns = (
- list.default_list_data(show_customer_portal_fields).get("columns")
+ _list.default_list_data(show_customer_portal_fields).get("columns")
if doctype == "HD Ticket"
- else list.default_list_data().get("columns")
+ else _list.default_list_data().get("columns")
)
- rows = list.default_list_data().get("rows")
+ rows = _list.default_list_data().get("rows")
if rows is None:
rows = []
@@ -173,6 +177,9 @@ def get_list_data(
if column.get("key") not in rows:
rows.append(column.get("key"))
+ if group_by_field and group_by_field not in rows:
+ rows.append(group_by_field)
+
rows.append("name") if "name" not in rows else rows
data = (
frappe.get_list(
@@ -221,12 +228,60 @@ def get_list_data(
if show_customer_portal_fields:
fields = get_customer_portal_fields(doctype, fields)
+ if group_by_field and view_type == "group_by":
+
+ def get_options(fieldtype, options):
+ if fieldtype == "Select":
+ return [option for option in options.split("\n")]
+ else:
+ has_empty_values = any([not d.get(group_by_field) for d in data])
+ options = list(set([d.get(group_by_field) for d in data]))
+ options = [u for u in options if u]
+ options = [category_name for category_name in options if category_name]
+ options = [
+ {
+ "label": frappe.db.get_value(
+ "HD Article Category", option, "category_name"
+ ),
+ "value": option,
+ }
+ for option in options
+ if option
+ ]
+ if has_empty_values:
+ options.append({"label": "", "value": ""})
+
+ if order_by and group_by_field in order_by:
+ order_by_fields = order_by.split(",")
+ order_by_fields = [
+ (field.split(" ")[0], field.split(" ")[1])
+ for field in order_by_fields
+ ]
+ if (group_by_field, "asc") in order_by_fields:
+ options.sort(key=lambda x: x.get("label"))
+ elif (group_by_field, "desc") in order_by_fields:
+ options.sort(reverse=True, key=lambda x: x.get("label"))
+ else:
+ options.sort(key=lambda x: x.get("label"))
+ return options
+
+ for field in fields:
+ if field.get("value") == group_by_field:
+ group_by_field = {
+ "label": field.get("label"),
+ "name": field.get("value"),
+ "type": field.get("type"),
+ "options": get_options(field.get("type"), field.get("options")),
+ }
+
return {
"data": data,
"columns": columns,
"fields": fields if doctype == "HD Ticket" else [],
"total_count": len(frappe.get_list(doctype, filters=filters)),
"row_count": len(data),
+ "group_by_field": group_by_field,
+ "view_type": view_type,
}
diff --git a/helpdesk/api/kbase.py b/helpdesk/api/kbase.py
index 65929085b..e5e51dc48 100644
--- a/helpdesk/api/kbase.py
+++ b/helpdesk/api/kbase.py
@@ -65,17 +65,14 @@ def get_sub_categories_and_articles(category):
}
category_tree["sub_categories"].append(sub_cat_tree)
- # add all articles to the main tree
-
+ all_articles = []
for sub_cat in category_tree["sub_categories"]:
- category_tree["all_articles"].extend(sub_cat["articles"])
+ all_articles.extend(sub_cat["articles"])
# get author details
- for article in category_tree["all_articles"]:
+ for article in all_articles:
author = article["author"]
if author not in category_tree["authors"]:
category_tree["authors"][author] = get_user_info_for_avatar(author)
- category_tree["children"] = direct_articles + category_tree["sub_categories"]
-
return category_tree
diff --git a/helpdesk/api/knowledge_base.py b/helpdesk/api/knowledge_base.py
new file mode 100644
index 000000000..7d9f1fc33
--- /dev/null
+++ b/helpdesk/api/knowledge_base.py
@@ -0,0 +1,65 @@
+import frappe
+from frappe import _
+from frappe.utils import get_user_info_for_avatar
+
+from helpdesk.utils import is_agent
+
+
+@frappe.whitelist(allow_guest=True)
+def get_article(name: str):
+ article = frappe.get_doc("HD Article", name).as_dict()
+
+ if not is_agent() and article["status"] != "Published":
+ frappe.throw(_("Access denied"), frappe.PermissionError)
+
+ author = get_user_info_for_avatar(article["author"])
+
+ return {
+ "name": article.name,
+ "title": article.title,
+ "content": article.content,
+ "author": author,
+ "creation": article.creation,
+ "status": article.status,
+ "published_on": article.published_on,
+ "modified": article.modified,
+ "category_name": frappe.db.get_value(
+ "HD Article Category", article.category, "category_name"
+ ),
+ "category_id": article.category,
+ }
+
+ return article
+
+
+@frappe.whitelist()
+def delete_articles(articles):
+ for article in articles:
+ frappe.delete_doc("HD Article", article)
+
+
+@frappe.whitelist()
+def create_category(title: str):
+ category = frappe.new_doc("HD Article Category", category_name=title).insert()
+ article = frappe.new_doc(
+ "HD Article", title="New Article", category=category.name
+ ).insert()
+ return {"article": article.name, "category": category.name}
+
+
+@frappe.whitelist()
+def delete_category(name: str):
+ try:
+ articles = frappe.get_all("HD Article", filters={"category": name})
+ for article in articles:
+ frappe.db.set_value("HD Article", article.name, "category", None)
+
+ frappe.delete_doc("HD Article Category", name)
+ except Exception as e:
+ frappe.db.rollback()
+
+
+@frappe.whitelist()
+def move_to_category(category, articles):
+ for article in articles:
+ frappe.db.set_value("HD Article", article, "category", category)
diff --git a/helpdesk/helpdesk/doctype/hd_agent/hd_agent.py b/helpdesk/helpdesk/doctype/hd_agent/hd_agent.py
index d3c06dc21..f632835ca 100644
--- a/helpdesk/helpdesk/doctype/hd_agent/hd_agent.py
+++ b/helpdesk/helpdesk/doctype/hd_agent/hd_agent.py
@@ -41,7 +41,7 @@ def default_list_data():
"type": "Datetime",
},
]
- rows = ["modified"]
+ rows = ["modified", "user.user_image"]
# modified row is needed because
# we have a link table for HD Agent to User
# and sql gets confused which modified to take from those 2 tables
diff --git a/helpdesk/helpdesk/doctype/hd_article/api.py b/helpdesk/helpdesk/doctype/hd_article/api.py
deleted file mode 100644
index a509b46fa..000000000
--- a/helpdesk/helpdesk/doctype/hd_article/api.py
+++ /dev/null
@@ -1,38 +0,0 @@
-import frappe
-from frappe import _
-
-from helpdesk.utils import is_agent
-
-
-@frappe.whitelist(allow_guest=True)
-def get_article(name: str):
- article = frappe.get_doc("HD Article", name).as_dict()
-
- if not is_agent() and article["status"] != "Published":
- frappe.throw(_("Access denied"), frappe.PermissionError)
-
- author = frappe.get_cached_doc("User", article["author"])
- sub_category = frappe.get_cached_doc("HD Article Category", article["category"])
- category = frappe.get_cached_doc(
- "HD Article Category", sub_category.parent_category or article["category"]
- )
-
- user = frappe.session.user
- # TODO: views count increment with views field in HD Article
- # if not is_agent() and user != author.name:
- # frappe.db.set_value("HD Article", name, "views", article["views"] + 1)
-
- user_feedback = int(
- frappe.db.get_value(
- "HD Article Feedback", {"user": user, "article": name}, "feedback"
- )
- or 0
- )
-
- return {
- **article,
- "author": author,
- "category": category,
- "sub_category": sub_category,
- "user_feedback": user_feedback,
- }
diff --git a/helpdesk/helpdesk/doctype/hd_article/hd_article.json b/helpdesk/helpdesk/doctype/hd_article/hd_article.json
index df87fc7b7..c80bacdd7 100644
--- a/helpdesk/helpdesk/doctype/hd_article/hd_article.json
+++ b/helpdesk/helpdesk/doctype/hd_article/hd_article.json
@@ -33,7 +33,6 @@
"fieldname": "category",
"fieldtype": "Link",
"in_list_view": 1,
- "in_standard_filter": 1,
"label": "Category",
"options": "HD Article Category"
},
@@ -79,6 +78,7 @@
"default": "Draft",
"fieldname": "status",
"fieldtype": "Select",
+ "in_standard_filter": 1,
"label": "Status",
"options": "Published\nDraft\nArchived"
},
@@ -101,7 +101,7 @@
],
"links": [],
"make_attachments_public": 1,
- "modified": "2024-11-25 13:02:28.644492",
+ "modified": "2025-01-07 00:59:35.927067",
"modified_by": "Administrator",
"module": "Helpdesk",
"name": "HD Article",
diff --git a/helpdesk/helpdesk/doctype/hd_article/hd_article.py b/helpdesk/helpdesk/doctype/hd_article/hd_article.py
index 1678d46d0..f8fa7ebec 100644
--- a/helpdesk/helpdesk/doctype/hd_article/hd_article.py
+++ b/helpdesk/helpdesk/doctype/hd_article/hd_article.py
@@ -59,7 +59,7 @@ def default_list_data():
"label": "Title",
"type": "Data",
"key": "title",
- "width": "17rem",
+ "width": "20rem",
},
{
"label": "Status",
diff --git a/helpdesk/helpdesk/doctype/hd_article_category/hd_article_category.py b/helpdesk/helpdesk/doctype/hd_article_category/hd_article_category.py
index f24f8f4ce..dfdd166e6 100644
--- a/helpdesk/helpdesk/doctype/hd_article_category/hd_article_category.py
+++ b/helpdesk/helpdesk/doctype/hd_article_category/hd_article_category.py
@@ -19,24 +19,3 @@ def before_save(self):
"HD Article Category", {"parent_category": self.parent_category}
)
)
-
- def archive(self):
- self.idx = -1
- self.status = "Archived"
- self.save()
-
- def unarchive(self):
- self.status = "Published"
- self.save()
-
- def get_breadcrumbs(self):
- breadcrumbs = [{"name": self.name, "label": self.category_name}]
- current_category = self
- while current_category.parent_category:
- current_category = frappe.get_doc(
- "HD Article Category", current_category.parent_category
- )
- breadcrumbs.append(
- {"name": current_category.name, "label": current_category.category_name}
- )
- return breadcrumbs[::-1]