Skip to content

Commit 233255e

Browse files
committed
feat: add column layout modification in general list view
1 parent 171dfa3 commit 233255e

File tree

5 files changed

+330
-4
lines changed

5 files changed

+330
-4
lines changed

desk/src/components/ListViewBuilder.vue

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
<Reload @click="reload" :loading="list.loading" />
1010
<Filter :default_filters="defaultParams.filters" />
1111
<SortBy :hide-label="isMobileView" />
12+
<ColumnSettings :hide-label="isMobileView" />
1213
</div>
1314
<div v-else class="flex justify-between items-center w-full">
1415
<Filter :default_filters="defaultParams.filters" />
@@ -111,13 +112,15 @@ import {
111112
SortBy,
112113
QuickFilters,
113114
Reload,
115+
ColumnSettings,
114116
} from "@/components/view-controls";
115117
import { MultipleAvatar, StarRating } from "@/components";
116118
import { dayjs } from "@/dayjs";
117119
import ListRows from "./ListRows.vue";
118120
import { useScreenSize } from "@/composables/screen";
119121
import EmptyState from "./EmptyState.vue";
120122
import { View } from "@/types";
123+
import { watch } from "vue";
121124
122125
interface P {
123126
options: {
@@ -173,6 +176,8 @@ const defaultParams = reactive({
173176
page_length: props.options.default_page_length,
174177
page_length_count: props.options.default_page_length,
175178
view: props.options.view,
179+
columns: [],
180+
rows: [],
176181
});
177182
178183
const emptyState = computed(() => {
@@ -377,7 +382,7 @@ provide("listViewData", listViewData);
377382
provide("listViewActions", {
378383
applyFilters,
379384
applySort,
380-
addColumn,
385+
updateColumns,
381386
reload,
382387
});
383388
@@ -391,8 +396,19 @@ function applySort(order_by: string) {
391396
list.submit({ ...defaultParams, order_by });
392397
}
393398
394-
function addColumn(field) {
395-
console.log("ADD COLUMN", field);
399+
function updateColumns(obj) {
400+
const { columns: _columns, isDefault, reload, reset } = obj;
401+
_columns?.forEach((column) => {
402+
handleFetchFromField(column);
403+
handleColumnConfig(column);
404+
return column;
405+
});
406+
columns.value = _columns;
407+
list.data.columns = _columns;
408+
defaultParams.columns = _columns;
409+
if (reload) {
410+
list.reload({ ...defaultParams });
411+
}
396412
}
397413
398414
function reload() {
@@ -416,7 +432,7 @@ function handlePageLength(count: number, loadMore: boolean = false) {
416432
list.reload();
417433
}
418434
419-
// to handle cases where the list view is updated
435+
// to handle cases where the list view is updated from the parent component
420436
defineExpose({
421437
reload,
422438
});

desk/src/components/icons/index.ts

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

desk/src/components/view-controls/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export { default as Filter } from "./Filter.vue";
22
export { default as SortBy } from "./SortBy.vue";
33
export { default as QuickFilters } from "./QuickFilters.vue";
44
export { default as Reload } from "./Reload.vue";
5+
export { default as ColumnSettings } from "./ColumnSettings.vue";

0 commit comments

Comments
 (0)