1<template>
2 <div class="flex flex-col flex-1">
3 <LayoutHeader>
4 <template #left-header>
5 <Breadcrumbs :items="breadcrumbs" />
6 </template>
7 <template #right-header> </template>
8 </LayoutHeader>
9 <div class="pt-6 mx-auto w-full max-w-4xl px-5">
10 <div class="flex flex-col gap-3 rounded-6 border w-full p-4">
11 <div class="flex justify-between items-center mb-3">
12 <!-- Author Info -->
13 <div class="flex gap-1 items-center flex-1 me-7 max-w-fit">
14 <UserAvatar :name="user.name" :expand="true" />
15 <span>{{ __("in") }}</span>
16 <Link
17 class="form-control"
18 doctype="OD Article Category"
19 :placeholder="__('Select Category')"
20 v-model="categoryId"
21 :pageLength="100"
22 :hide-clear-button="true"
23 />
24 </div>
25 <!-- Action Buttons -->
26 <div class="flex gap-2">
27 <Button :label="__('Discard')" @click="handleArticleDiscard" />
28 <Button
29 :label="__('Create')"
30 variant="solid"
31 @click="handleCreateArticle"
32 />
33 </div>
34 </div>
35 <!-- Title -->
36 <textarea
37 class="w-full resize-none border-0 bg-transparent text-3xl-bold placeholder-ink-gray-3 p-0 pb-3 border-b border-outline-elevation-2 focus:ring-0 focus:border-outline-elevation-2"
38 v-model="title"
39 :placeholder="__('Title')"
40 rows="1"
41 wrap="soft"
42 maxlength="140"
43 autofocus
44 @input="
45 (e: Event) => {
46 const target = e.target as HTMLTextAreaElement;
47 target.style.height = `${target.scrollHeight}px`;
48 }
49 "
50 />
51 <!-- Article Content -->
52 <Editor
53 v-model="content"
54 :extensions="extensions"
55 :upload-function="
56 (file: any, options: any) =>
57 uploadFunction(file, 'OD Article', null, false, options)
58 "
59 :placeholder="__('Write your article here...')"
60 >
61 <template #default>
62 <EditorContent
63 class="rounded-b-6 max-w-[unset] prose-sm h-[calc(100vh-340px)] sm:h-[calc(100vh-250px)] overflow-auto"
64 :class="ARTICLE_IMAGE_CLASSES"
65 />
66 <EditorFixedMenu
67 class="-ms-1 overflow-x-auto w-full"
68 :items="fullToolbar"
69 />
70 <EditorTableMenu />
71 </template>
72 </Editor>
73 </div>
74 </div>
75 </div>
76</template>
77
78<script setup lang="ts">
79import { Breadcrumbs, toast, usePageMeta } from "frappe-ui";
80import {
81 Editor,
82 EditorContent,
83 EditorFixedMenu,
84 EditorTableMenu,
85} from "frappe-ui/editor";
86import { buildEditorExtensions, fullToolbar } from "@/components/editor/config";
87import { ARTICLE_IMAGE_CLASSES } from "@opendesk/shared/tiptap";
88const extensions = buildEditorExtensions();
89import { useOnboarding } from "@framework/ui";
90import Link from "@/components/frappe-ui/Link.vue";
91import { computed, ref } from "vue";
92import { __ } from "@/translation";
93
94import { LayoutHeader, UserAvatar } from "@/components";
95import { useAuthStore } from "@/stores/auth";
96import { globalStore } from "@/stores/globalStore";
97import { newArticle } from "@/stores/knowledgeBase";
98import { useUserStore } from "@/stores/user";
99import { Article } from "@/types";
100import { uploadFunction } from "@/utils";
101import { useRoute, useRouter } from "vue-router";
102
103const userStore = useUserStore();
104const user = userStore.getUser();
105const { $dialog } = globalStore();
106const router = useRouter();
107const route = useRoute();
108const { updateOnboardingStep } = useOnboarding("opendesk") ?? {};
109const { isManager } = useAuthStore();
110
111const title = ref("");
112const content = ref("");
113
114const props = defineProps({
115 id: {
116 type: String,
117 required: true,
118 },
119});
120
121const categoryId = ref(props.id || null);
122const categoryName = computed(() => (route.query.title as string) || "");
123
124function handleCreateArticle() {
125 newArticle.submit(
126 { title: title.value, content: content.value, category: categoryId.value },
127 {
128 onSuccess: (article: Article) => {
129 toast.success(__("Article created successfully."));
130 if (isManager) {
131 updateOnboardingStep?.("first_article");
132 }
133 resetState();
134 router.push({
135 name: "Article",
136 params: {
137 articleId: article.name,
138 },
139 });
140 },
141 onError: (error: string) => {
142 toast.error(error);
143 },
144 }
145 );
146}
147function handleArticleDiscard() {
148 if (!title.value && !content.value) {
149 router.push({
150 name: "AgentKnowledgeBase",
151 });
152 return;
153 }
154 $dialog({
155 title: __("Discard Article"),
156 message: __("Are you sure you want to discard this article?"),
157 actions: [
158 {
159 label: __("Confirm"),
160 variant: "solid",
161 onClick({ close }: { close: () => void }) {
162 router.push({
163 name: "AgentKnowledgeBase",
164 });
165 resetState();
166 close();
167 },
168 },
169 ],
170 });
171}
172
173function resetState() {
174 title.value = "";
175 content.value = "";
176}
177
178const breadcrumbs = computed(() => {
179 const options: Array<{ label: string; route?: { name: string } }> = [
180 {
181 label: __("Knowledge Base"),
182 route: { name: "AgentKnowledgeBase" },
183 },
184 ];
185 options.push({
186 label: __("New Article"),
187 });
188 return options;
189});
190
191usePageMeta(() => {
192 return {
193 title: __("New Article"),
194 };
195});
196</script>
197