1<template>
2 <div class="flex flex-col gap-3">
3 <div class="flex items-start justify-between gap-4">
4 <div class="flex min-w-0 flex-col">
5 <h3 class="text-base-medium text-ink-gray-8">
6 {{ __("Pinned categories") }}
7 </h3>
8 <span class="mt-1 text-base leading-5 text-ink-gray-6">
9 {{
10 __(
11 "Pinned categories will appear first on your knowledge base home page, in this order."
12 )
13 }}
14 </span>
15 </div>
16 <Button class="shrink-0" :label="__('Customize')" @click="open = true" />
17 </div>
18 <div
19 v-if="pinned.length"
20 class="rounded-5 border border-outline-gray-2 px-1"
21 >
22 <div
23 v-for="(name, index) in pinned"
24 :key="name"
25 class="border-b border-outline-gray-1 py-1 last:border-b-0"
26 >
27 <div
28 class="group flex h-11 items-center gap-2.5 rounded-4 pl-2 pr-1.5 hover:bg-surface-gray-1 sm:h-10"
29 >
30 <span
31 :class="byName[name]?.icon || 'lucide-folder'"
32 class="size-4 shrink-0 text-ink-gray-6"
33 />
34 <span class="min-w-0 flex-1 truncate text-base text-ink-gray-8">
35 {{ labelOf(name) }}
36 </span>
37 <CategoryAccessBadge :access="byName[name]?.access" />
38 <Dropdown align="end" :options="rowOptions(index)">
39 <Button
40 variant="ghost"
41 icon="lucide-more-horizontal"
42 :label="__('Options for {0}', [labelOf(name)])"
43 />
44 </Dropdown>
45 </div>
46 </div>
47 </div>
48 <!-- A row's height, so the first pin shifts nothing below. -->
49 <div v-else class="rounded-5 border border-outline-gray-2 px-1 py-1">
50 <div
51 class="flex min-h-11 items-center justify-center px-3 text-center text-p-base text-ink-gray-6 sm:min-h-10"
52 >
53 {{
54 __(
55 "Nothing pinned yet. The home page will list all categories alphabetically."
56 )
57 }}
58 </div>
59 </div>
60 <PinnedCategoriesDialog
61 v-model:open="open"
62 :categories="categories"
63 :pinned="pinned"
64 :save="save"
65 />
66 </div>
67</template>
68
69<script setup lang="ts">
70import { __ } from "../translation";
71import { Button, Dropdown } from "frappe-ui";
72import { computed, ref } from "vue";
73import CategoryAccessBadge from "./CategoryAccessBadge.vue";
74import PinnedCategoriesDialog from "./PinnedCategoriesDialog.vue";
75
76type Category = {
77 label: string;
78 value: string;
79 icon: string;
80 access?: string;
81};
82
83const props = defineProps<{
84 categories: Category[];
85 pinned: string[];
86 // The Customize dialog saves at once; resolves true on success.
87 save: (pinned: string[]) => Promise<boolean>;
88}>();
89
90// Row menu edits wait for the form's Save.
91const emit = defineEmits<{ "update:pinned": [pinned: string[]] }>();
92
93const open = ref(false);
94
95const byName = computed(() =>
96 Object.fromEntries(props.categories.map((row) => [row.value, row]))
97);
98
99function labelOf(name: string) {
100 return byName.value[name]?.label || name;
101}
102
103function moveTo(name: string, index: number) {
104 const next = props.pinned.filter((row) => row !== name);
105 next.splice(index, 0, name);
106 emit("update:pinned", next);
107}
108
109function rowOptions(index: number) {
110 const name = props.pinned[index];
111 return [
112 {
113 label: __("Move up"),
114 icon: "lucide-arrow-up",
115 disabled: index === 0,
116 onClick: () => moveTo(name, index - 1),
117 },
118 {
119 label: __("Move down"),
120 icon: "lucide-arrow-down",
121 disabled: index === props.pinned.length - 1,
122 onClick: () => moveTo(name, index + 1),
123 },
124 {
125 label: __("Unpin"),
126 icon: "lucide-pin-off",
127 onClick: () =>
128 emit(
129 "update:pinned",
130 props.pinned.filter((row) => row !== name)
131 ),
132 },
133 ];
134}
135</script>
136