1<template>
2 <Popover bare :offset="8">
3 <template #trigger>
4 <Button :label="__('Columns')">
5 <template v-if="hideLabel" #icon>
6 <ColumnsIcon class="h-4" />
7 </template>
8 <template v-else #prefix>
9 <ColumnsIcon class="h-4" />
10 </template>
11 </Button>
12 </template>
13 <template #default="{ close }">
14 <div
15 class="p-1.5 min-w-40 rounded-6 bg-surface-elevation-2 shadow-2xl ring-1 ring-black ring-opacity-5 focus:outline-none"
16 >
17 <div v-if="!edit">
18 <Draggable
19 :list="columns"
20 @end="apply"
21 :delay="isTouchScreenDevice() ? 200 : 0"
22 item-key="key"
23 class="list-group"
24 >
25 <template #item="{ element }">
26 <div
27 class="flex cursor-grab items-center justify-between gap-6 rounded-4 px-2 py-1.5 text-base text-ink-gray-8 hover:bg-surface-gray-2"
28 >
29 <div class="flex items-center gap-2">
30 <DragIcon class="h-3.5" />
31 <div>{{ __(element.label) }}</div>
32 </div>
33 <div class="flex cursor-pointer items-center gap-0.5">
34 <Button
35 variant="ghost"
36 class="!h-5 w-5 !p-1"
37 @click="editColumn(element)"
38 >
39 <template #icon>
40 <EditIcon class="h-3.5" />
41 </template>
42 </Button>
43 <Button
44 variant="ghost"
45 class="!h-5 w-5 !p-1"
46 @click="removeColumn(element)"
47 >
48 <template #icon>
49 <LucideX class="size-3.5" />
50 </template>
51 </Button>
52 </div>
53 </div>
54 </template>
55 </Draggable>
56 <div
57 class="mt-1.5 flex flex-col gap-1 border-t border-outline-elevation-2 pt-1.5"
58 >
59 <Combobox
60 :model-value="null"
61 :options="fields"
62 align="end"
63 @update:selected-option="(option) => option && addColumn(option)"
64 >
65 <template #trigger>
66 <Button
67 class="w-full !justify-start !text-ink-gray-5"
68 variant="ghost"
69 :label="__('Add Column')"
70 >
71 <template #prefix>
72 <LucidePlus class="size-4" />
73 </template>
74 </Button>
75 </template>
76 </Combobox>
77 <!-- <Button
78 v-if="columnsUpdated"
79 class="w-full !justify-start !text-ink-gray-5"
80 variant="ghost"
81 @click="reset(close)"
82 label="Reset Changes"
83 >
84 <template #prefix>
85 <ReloadIcon class="h-4" />
86 </template>
87 </Button> -->
88 <Button
89 v-if="!is_default"
90 class="w-full !justify-start !text-ink-gray-5"
91 variant="ghost"
92 @click="resetToDefault(close)"
93 :label="__('Reset to Default')"
94 >
95 <template #prefix>
96 <ReloadIcon class="h-4" />
97 </template>
98 </Button>
99 </div>
100 </div>
101 <div v-else>
102 <div
103 class="flex flex-col items-center justify-between gap-2 rounded-4 px-2 py-1.5 text-base text-ink-gray-8"
104 >
105 <div class="flex flex-col items-center gap-3">
106 <FormControl
107 type="text"
108 size="md"
109 label="Label"
110 v-model="column.label"
111 class="sm:w-full w-52"
112 placeholder="First Name"
113 />
114 <FormControl
115 type="text"
116 size="md"
117 label="Width"
118 class="sm:w-full w-52"
119 v-model="column.width"
120 placeholder="10rem"
121 :description="'Width can be in number, pixel or rem (eg. 3, 30px, 10rem)'"
122 :debounce="500"
123 />
124 </div>
125 <div class="flex w-full gap-2 border-t pt-2">
126 <Button
127 variant="subtle"
128 label="Cancel"
129 class="w-full flex-1"
130 @click="cancelUpdate"
131 />
132 <Button
133 variant="solid"
134 label="Update"
135 class="w-full flex-1"
136 @click="updateColumn(column)"
137 />
138 </div>
139 </div>
140 </div>
141 </div>
142 </template>
143 </Popover>
144</template>
145
146<script setup>
147import LucidePlus from "~icons/lucide/plus";
148import {
149 ColumnsIcon,
150 DragIcon,
151 EditIcon,
152 ReloadIcon,
153} from "@/components/icons";
154import { isTouchScreenDevice } from "@/utils";
155import { watchOnce } from "@vueuse/core";
156import { Button, Combobox, FormControl, Popover } from "frappe-ui";
157import { computed, inject, ref } from "vue";
158import Draggable from "vuedraggable";
159
160const props = defineProps({
161 hideLabel: {
162 type: Boolean,
163 default: false,
164 },
165});
166
167const emit = defineEmits(["update"]);
168const columnsUpdated = ref(false);
169
170const oldValues = ref({
171 columns: [],
172 rows: [],
173 isDefault: false,
174});
175
176const listViewData = inject("listViewData");
177const { list } = listViewData;
178const listViewActions = inject("listViewActions");
179
180const edit = ref(false);
181const column = ref({
182 old: {},
183 label: "",
184 key: "",
185 width: "10rem",
186});
187
188const is_default = computed({
189 get: () => list.data?.is_default,
190 set: (val) => {
191 list.data.is_default = val;
192 },
193});
194
195const columns = computed({
196 get: () => list.data?.columns,
197 set: (val) => {
198 list.data.columns = val;
199 },
200});
201
202const rows = computed({
203 get: () => list.data?.rows,
204 set: (val) => {
205 list.data.rows = val;
206 },
207});
208
209const fields = computed(() => {
210 let allFields = list.data?.fields;
211 if (!allFields) return [];
212 if (columns.value === "") return allFields;
213 return allFields.filter((field) => {
214 return !columns.value.find((column) => column.key === field.value);
215 });
216});
217
218function addColumn(c) {
219 // Combobox normalisation overwrites the option's `type` with "option", so
220 // read the real fieldtype back from the source list.
221 const type = list.data?.fields?.find((f) => f.value === c.value)?.type;
222 let align = ["Float", "Int", "Percent", "Currency"].includes(type)
223 ? "right"
224 : "left";
225 let _column = {
226 label: c.label,
227 type,
228 key: c.value,
229 width: "10rem",
230 align,
231 };
232 columns.value.push(_column);
233 rows.value.push(c.value);
234 apply(true);
235}
236
237function removeColumn(c) {
238 if (columns.value.length === 1) {
239 return;
240 }
241 columns.value = columns.value.filter((column) => column.key !== c.key);
242 if (c.key !== "name") {
243 rows.value = rows.value.filter((row) => row !== c.key);
244 }
245 apply();
246}
247
248function editColumn(c) {
249 edit.value = true;
250 column.value = c;
251 column.value.old = { ...c };
252}
253
254function updateColumn(c) {
255 edit.value = false;
256 let index = columns.value.findIndex((column) => column.key === c.key);
257 columns.value[index].label = c.label;
258 columns.value[index].width = c.width;
259
260 if (columns.value[index].old) {
261 delete columns.value[index].old;
262 }
263 apply();
264}
265
266function cancelUpdate() {
267 edit.value = false;
268 column.value.label = column.value.old.label;
269 column.value.width = column.value.old.width;
270 delete column.value.old;
271}
272
273function reset(close) {
274 apply(true, false, true);
275 close();
276}
277
278function resetToDefault(close) {
279 apply(true, true, true);
280 close();
281}
282
283function apply(reload = false, isDefault = false, reset = false) {
284 is_default.value = isDefault;
285 columnsUpdated.value = true;
286 let obj = {
287 columns: reset ? oldValues.value.columns : columns.value,
288 rows: reset ? oldValues.value.rows : rows.value,
289 isDefault: reset ? oldValues.value.isDefault : isDefault,
290 reload,
291 reset,
292 };
293 listViewActions.updateColumns(obj);
294 columnsUpdated.value = !reset;
295}
296
297watchOnce(
298 () => list.data,
299 (val) => {
300 if (!val) return;
301 oldValues.value.columns = JSON.parse(JSON.stringify(val.columns));
302 oldValues.value.rows = JSON.parse(JSON.stringify(val.rows));
303 oldValues.value.isDefault = val.is_default;
304 }
305);
306</script>
307