View | Details | Raw Unified | Return to bug 39320
Collapse All | Expand All

(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/components/ModuleDashboard/ModuleDashboard.vue (+222 lines)
Line 0 Link Here
1
<template>
2
    <div>
3
        <div id="dashboard-header">
4
            <p>
5
                {{ $__("Customize your dashboard by adding widgets.") }}
6
                <a href="#" id="open-widget-picker" @click="toggleWidgetPicker">
7
                    {{ $__("Open Widget Picker") }}
8
                </a>
9
            </p>
10
        </div>
11
    </div>
12
    <div class="row">
13
        <div class="col-md-6 dashboard-left-col">
14
            <draggable
15
                :ghost="true"
16
                @drag="handleDrag"
17
                class="dragArea list-group w-full"
18
                :list="selectedWidgetsLeft"
19
                group="widgets"
20
            >
21
                <component
22
                    v-for="(widget, index) in selectedWidgetsLeft"
23
                    :key="widget.name"
24
                    :is="widget"
25
                    display="dashboard"
26
                    dashboardColumn="left"
27
                    @moveWidget="moveWidget"
28
                    @removed="removeWidget(widget)"
29
                ></component>
30
            </draggable>
31
        </div>
32
        <div class="col-md-6 dashboard-right-col">
33
            <draggable
34
                :ghost="true"
35
                @drag="handleDrag"
36
                class="dragArea list-group w-full"
37
                :list="selectedWidgetsRight"
38
                group="widgets"
39
            >
40
                <component
41
                    v-for="(widget, index) in selectedWidgetsRight"
42
                    :key="widget.name"
43
                    :is="widget"
44
                    display="dashboard"
45
                    dashboardColumn="right"
46
                    @moveWidget="moveWidget"
47
                    @removed="removeWidget(widget)"
48
                ></component>
49
            </draggable>
50
        </div>
51
    </div>
52
</template>
53
54
<script>
55
import { onMounted, ref, watch, inject } from "vue";
56
import WidgetPicker from "./WidgetPicker.vue";
57
import VueCookies from "vue-cookies";
58
import { $__ } from "@koha-vue/i18n";
59
import { VueDraggableNext } from "vue-draggable-next";
60
61
export default {
62
    setup(props) {
63
        const availableWidgets = props.availableWidgets;
64
        const selectedWidgetsLeft = ref([]);
65
        const selectedWidgetsRight = ref([]);
66
67
        const { setComponentDialog } = inject("mainStore");
68
69
        const handleDrag = event => {
70
            const dropContext = event.relatedContext;
71
            if (dropContext) {
72
                const dropElement = dropContext.element;
73
                dropElement.style.border = "2px dotted #ccc";
74
                event.onDragEnd(() => {
75
                    dropElement.style.border = "";
76
                });
77
            }
78
        };
79
80
        function removeWidget(widget) {
81
            const indexLeft = selectedWidgetsLeft.value.indexOf(widget);
82
            const indexRight = selectedWidgetsRight.value.indexOf(widget);
83
            if (indexLeft > -1) {
84
                selectedWidgetsLeft.value.splice(indexLeft, 1);
85
            } else if (indexRight > -1) {
86
                selectedWidgetsRight.value.splice(indexRight, 1);
87
            }
88
        }
89
90
        function addWidget(widget) {
91
            if (
92
                !selectedWidgetsLeft.value.includes(widget) &&
93
                !selectedWidgetsRight.value.includes(widget)
94
            ) {
95
                if (
96
                    selectedWidgetsLeft.value.length <=
97
                    selectedWidgetsRight.value.length
98
                ) {
99
                    selectedWidgetsLeft.value.unshift(widget);
100
                } else {
101
                    selectedWidgetsRight.value.unshift(widget);
102
                }
103
            }
104
        }
105
106
        function moveWidget(widget) {
107
            if (
108
                selectedWidgetsLeft.value.some(
109
                    element => element.name === widget.componentName
110
                )
111
            ) {
112
                selectedWidgetsRight.value.unshift(
113
                    selectedWidgetsLeft.value.find(
114
                        element => element.name === widget.componentName
115
                    )
116
                );
117
                selectedWidgetsLeft.value = selectedWidgetsLeft.value.filter(
118
                    element => element.name !== widget.componentName
119
                );
120
            } else if (
121
                selectedWidgetsRight.value.some(
122
                    element => element.name === widget.componentName
123
                )
124
            ) {
125
                selectedWidgetsLeft.value.unshift(
126
                    selectedWidgetsRight.value.find(
127
                        element => element.name === widget.componentName
128
                    )
129
                );
130
                selectedWidgetsRight.value = selectedWidgetsRight.value.filter(
131
                    element => element.name !== widget.componentName
132
                );
133
            }
134
        }
135
136
        const toggleWidgetPicker = e => {
137
            setComponentDialog({
138
                title: $__("Customize your dashboard widgets"),
139
                cancel_label: $__("Close"),
140
                componentPath:
141
                    "@koha-vue/components/ModuleDashboard/WidgetPicker.vue",
142
                componentProps: {
143
                    selectedWidgetsLeft: selectedWidgetsLeft.value,
144
                    selectedWidgetsRight: selectedWidgetsRight.value,
145
                    availableWidgets: availableWidgets,
146
                },
147
                componentListeners: {
148
                    added: widget => addWidget(widget),
149
                    removed: widget => removeWidget(widget),
150
                },
151
            });
152
        };
153
154
        onMounted(() => {
155
            const storedWidgets = VueCookies.get("dashboard-widgets");
156
            if (storedWidgets) {
157
                const { left, right } = storedWidgets;
158
                left.forEach(widgetName => {
159
                    const widget = availableWidgets.find(
160
                        widget => widget.name === widgetName
161
                    );
162
                    if (widget) {
163
                        selectedWidgetsLeft.value.push(widget);
164
                    }
165
                });
166
                right.forEach(widgetName => {
167
                    const widget = availableWidgets.find(
168
                        widget => widget.name === widgetName
169
                    );
170
                    if (widget) {
171
                        selectedWidgetsRight.value.push(widget);
172
                    }
173
                });
174
            } else {
175
                availableWidgets.forEach(widget => addWidget(widget));
176
            }
177
        });
178
179
        watch(
180
            [selectedWidgetsLeft, selectedWidgetsRight],
181
            ([left, right]) => {
182
                const leftWidgetNames = left.map(widget => widget.name);
183
                const rightWidgetNames = right.map(widget => widget.name);
184
                VueCookies.set(
185
                    "dashboard-widgets",
186
                    JSON.stringify({
187
                        left: leftWidgetNames,
188
                        right: rightWidgetNames,
189
                    }),
190
                    "30d"
191
                );
192
            },
193
            { deep: true }
194
        );
195
196
        return {
197
            ...props,
198
            selectedWidgetsLeft,
199
            selectedWidgetsRight,
200
            addWidget,
201
            handleDrag,
202
            removeWidget,
203
            moveWidget,
204
            toggleWidgetPicker,
205
        };
206
    },
207
    components: {
208
        WidgetPicker,
209
        draggable: VueDraggableNext,
210
    },
211
    name: "ModuleDashboard",
212
};
213
</script>
214
<style>
215
.sortable-ghost {
216
    height: 50px !important;
217
}
218
219
.sortable-ghost.sortable-chosen > * {
220
    visibility: hidden;
221
}
222
</style>
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/components/ModuleDashboard/WidgetDashboardWrapper.vue (+231 lines)
Line 0 Link Here
1
<template>
2
    <div class="widget" :id="id">
3
        <div class="widget-header">
4
            <div class="row align-items-center">
5
                <div class="col-md-6">
6
                    <h1 class="widget-title m-0">
7
                        <i v-if="icon" :icon="icon" :class="icon"></i>
8
                        {{ name }}
9
                    </h1>
10
                </div>
11
                <div class="col-md-6 text-end">
12
                    <div class="dropdown">
13
                        <button
14
                            class="btn btn-default"
15
                            type="button"
16
                            id="dropdownMenuButton"
17
                            data-bs-toggle="dropdown"
18
                            aria-expanded="false"
19
                        >
20
                            <font-awesome-icon icon="ellipsis-vertical" />
21
                        </button>
22
                        <ul
23
                            class="dropdown-menu"
24
                            aria-labelledby="dropdownMenuButton"
25
                        >
26
                            <li>
27
                                <button
28
                                    v-if="
29
                                        Object.keys(settings_definitions).length
30
                                    "
31
                                    class="dropdown-item toggle-settings"
32
                                    @mousedown="toggleSettings"
33
                                    :title="$__('Settings')"
34
                                >
35
                                    <font-awesome-icon icon="cog" />
36
                                    {{ $__("Settings") }}
37
                                </button>
38
                            </li>
39
                            <li v-if="dashboardColumn === 'left'">
40
                                <button
41
                                    class="dropdown-item move-right"
42
                                    @mousedown="moveWidget('right')"
43
                                    :title="$__('Move to right')"
44
                                >
45
                                    <font-awesome-icon icon="arrow-right" />
46
                                    {{ $__("Move to right") }}
47
                                </button>
48
                            </li>
49
                            <li v-if="dashboardColumn === 'right'">
50
                                <button
51
                                    class="dropdown-item move-left"
52
                                    @mousedown="moveWidget('left')"
53
                                    :title="$__('Move to left')"
54
                                >
55
                                    <font-awesome-icon icon="arrow-left" />
56
                                    {{ $__("Move to left") }}
57
                                </button>
58
                            </li>
59
                            <li>
60
                                <button
61
                                    class="dropdown-item remove-widget"
62
                                    @mousedown="removeWidget"
63
                                    :title="$__('Remove')"
64
                                >
65
                                    <font-awesome-icon icon="trash" />
66
                                    {{ $__("Remove") }}
67
                                </button>
68
                            </li>
69
                        </ul>
70
                    </div>
71
                </div>
72
            </div>
73
        </div>
74
        <div v-if="showSettings" class="widget-settings">
75
            <h3 class="widget-title">{{ $__("Settings") }}</h3>
76
            <form>
77
                <fieldset class="rows">
78
                    <ol>
79
                        <li
80
                            v-for="(setting, index) in settings_definitions"
81
                            v-bind:key="index"
82
                        >
83
                            <FormElement
84
                                :resource="settings"
85
                                :attr="setting"
86
                                :index="index"
87
                            />
88
                        </li>
89
                    </ol>
90
                </fieldset>
91
            </form>
92
            <div class="text-end">
93
                <button
94
                    type="button"
95
                    class="btn btn-default"
96
                    @click="toggleSettings"
97
                >
98
                    {{ $__("Close settings") }}
99
                </button>
100
            </div>
101
        </div>
102
        <div class="widget-content">
103
            <div v-if="loading" class="text-center">
104
                {{ $__("Loading...") }}
105
            </div>
106
            <slot v-else></slot>
107
        </div>
108
    </div>
109
</template>
110
111
<script>
112
import { ref } from "vue";
113
import FormElement from "../FormElement.vue";
114
export default {
115
    props: {
116
        id: {
117
            type: String,
118
            required: true,
119
        },
120
        name: {
121
            type: String,
122
            required: true,
123
        },
124
        icon: {
125
            type: String,
126
            required: false,
127
        },
128
        loading: {
129
            type: Boolean,
130
        },
131
        settings: {
132
            type: Object,
133
            default: () => ({}),
134
        },
135
        settings_definitions: {
136
            type: Object,
137
            default: () => ({}),
138
        },
139
        dashboardColumn: {
140
            type: String,
141
            required: false,
142
        },
143
    },
144
    setup(props, { emit }) {
145
        const showSettings = ref(false);
146
147
        const removeWidget = () => {
148
            emit("removed", props);
149
        };
150
151
        const moveWidget = column => {
152
            emit("moveWidget");
153
        };
154
155
        function toggleSettings() {
156
            showSettings.value = !showSettings.value;
157
        }
158
159
        return {
160
            showSettings,
161
            toggleSettings,
162
            removeWidget,
163
            moveWidget,
164
        };
165
    },
166
    emits: ["removed", "moveWidget"],
167
    components: {
168
        FormElement,
169
    },
170
};
171
</script>
172
173
<style scoped>
174
.widget {
175
    background-color: #fff;
176
    border: 1px solid #ccc;
177
    border-radius: 8px;
178
    padding: 16px;
179
    margin-bottom: 12px;
180
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
181
}
182
183
.widget-header {
184
    background-color: #f7f7f7;
185
    border-bottom: 1px solid #ccc;
186
    border-top-left-radius: 8px;
187
    border-top-right-radius: 8px;
188
}
189
190
.widget-settings {
191
    border-bottom: 1px solid #ccc;
192
    border-top-left-radius: 8px;
193
    border-top-right-radius: 8px;
194
}
195
196
.widget-settings fieldset.rows ol {
197
    padding: 0;
198
}
199
200
.widget-settings form:after {
201
    content: "";
202
    display: table;
203
    clear: both;
204
}
205
206
:deep(.widget-settings form .v-select) {
207
    display: inline-block;
208
    background-color: white;
209
    width: 60%;
210
}
211
212
:deep(.v-select),
213
:deep(
214
    input:not([type="submit"]):not([type="search"]):not([type="button"]):not(
215
            [type="checkbox"]
216
        ):not([type="radio"])
217
),
218
:deep(textarea) {
219
    border-color: rgba(60, 60, 60, 0.26);
220
    border-width: 1px;
221
    border-radius: 4px;
222
    min-width: 60%;
223
}
224
225
.widget-content,
226
.widget-settings,
227
.widget-header {
228
    padding: 6px 12px;
229
    margin-bottom: 6px;
230
}
231
</style>
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/components/ModuleDashboard/WidgetPicker.vue (+59 lines)
Line 0 Link Here
1
<template>
2
    <div v-for="widget in availableWidgets" :key="widget" class="widget-item">
3
        <component
4
            display="picker"
5
            :is="widget"
6
            :alreadyAdded="alreadyOnDashboard(widget)"
7
            @removed="removeWidget(widget)"
8
            @added="addWidget(widget)"
9
        ></component>
10
    </div>
11
</template>
12
13
<script>
14
export default {
15
    props: {
16
        availableWidgets: {
17
            type: Array,
18
            required: true,
19
        },
20
        selectedWidgetsLeft: {
21
            type: Array,
22
            required: true,
23
        },
24
        selectedWidgetsRight: {
25
            type: Array,
26
            required: true,
27
        },
28
    },
29
30
    setup(props, { emit }) {
31
        const availableWidgets = props.availableWidgets;
32
        const selectedWidgetsLeft = props.selectedWidgetsLeft;
33
        const selectedWidgetsRight = props.selectedWidgetsRight;
34
35
        const removeWidget = widget => {
36
            emit("removed", widget);
37
        };
38
        const addWidget = widget => {
39
            emit("added", widget);
40
        };
41
42
        const alreadyOnDashboard = widget => {
43
            return (
44
                selectedWidgetsLeft.includes(widget) ||
45
                selectedWidgetsRight.includes(widget)
46
            );
47
        };
48
49
        return {
50
            ...props,
51
            addWidget,
52
            alreadyOnDashboard,
53
            removeWidget,
54
        };
55
    },
56
    emits: ["added", "removed"],
57
    name: "WidgetPicker",
58
};
59
</script>
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/components/ModuleDashboard/WidgetPickerWrapper.vue (+97 lines)
Line 0 Link Here
1
<template>
2
    <div class="widget-details" :id="id">
3
        <div class="d-flex align-items-center widget-header">
4
            <h1 class="widget-title me-auto m-0">
5
                <i v-if="icon" :class="icon"></i> {{ name }}
6
            </h1>
7
            <div class="widget-actions">
8
                <button
9
                    v-if="!alreadyAdded"
10
                    class="btn btn-primary me-1 add-widget"
11
                    @click="addWidget"
12
                >
13
                    <font-awesome-icon icon="plus" />
14
                    {{ $__("Add") }}
15
                </button>
16
                <button
17
                    v-if="alreadyAdded"
18
                    class="btn btn-default remove-widget"
19
                    @click="removeWidget"
20
                >
21
                    <font-awesome-icon icon="trash" />
22
                    {{ $__("Remove") }}
23
                </button>
24
            </div>
25
        </div>
26
        <p class="widget-description">
27
            {{ description }}
28
        </p>
29
    </div>
30
</template>
31
32
<script>
33
import { toRefs } from "vue";
34
35
export default {
36
    name: "WidgetDetails",
37
    props: {
38
        id: {
39
            type: String,
40
            required: true,
41
        },
42
        name: {
43
            type: String,
44
            required: true,
45
        },
46
        icon: {
47
            type: String,
48
            required: false,
49
        },
50
        description: {
51
            type: String,
52
            required: true,
53
        },
54
        alreadyAdded: {
55
            type: Boolean,
56
            default: false,
57
        },
58
    },
59
    emits: ["removed", "added"],
60
    setup(props, { emit }) {
61
        const { id, name, icon, description, alreadyAdded } = toRefs(props);
62
63
        function removeWidget() {
64
            emit("removed", props);
65
        }
66
67
        function addWidget() {
68
            emit("added", props);
69
        }
70
71
        return {
72
            id,
73
            name,
74
            icon,
75
            description,
76
            alreadyAdded,
77
            removeWidget,
78
            addWidget,
79
        };
80
    },
81
};
82
</script>
83
84
<style scoped>
85
.widget-description {
86
    padding: 12px;
87
}
88
89
.widget-header {
90
    background-color: #f7f7f7;
91
    border-bottom: 1px solid #ccc;
92
    border-top-left-radius: 8px;
93
    border-top-right-radius: 8px;
94
    padding: 6px 12px;
95
    margin-bottom: 6px;
96
}
97
</style>
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/components/ModuleDashboard/WidgetWrapper.vue (+136 lines)
Line 0 Link Here
1
<!-- WidgetWrapper.vue -->
2
<template>
3
    <template v-if="display === 'picker'">
4
        <WidgetPickerWrapper
5
            :id="id"
6
            :alreadyAdded="alreadyAdded"
7
            @added="handleAddWidget"
8
            @removed="handleRemoveWidget"
9
            :name="name"
10
            :icon="icon"
11
            :description="description"
12
        />
13
    </template>
14
15
    <template v-else-if="display === 'dashboard'">
16
        <WidgetDashboardWrapper
17
            @removed="handleRemoveWidget"
18
            @moveWidget="handleMoveWidget"
19
            :id="id"
20
            :settings="settings"
21
            :settings_definitions="settings_definitions"
22
            :loading="loading"
23
            :dashboardColumn="dashboardColumn"
24
            :name="name"
25
            :icon="icon"
26
        >
27
            <slot />
28
        </WidgetDashboardWrapper>
29
    </template>
30
</template>
31
32
<script>
33
import { inject, toRefs } from "vue";
34
import WidgetDashboardWrapper from "./WidgetDashboardWrapper.vue";
35
import WidgetPickerWrapper from "./WidgetPickerWrapper.vue";
36
37
export default {
38
    name: "WidgetWrapper",
39
    components: {
40
        WidgetDashboardWrapper,
41
        WidgetPickerWrapper,
42
    },
43
    props: {
44
        display: {
45
            type: String,
46
            required: true,
47
        },
48
        alreadyAdded: {
49
            type: Boolean,
50
            default: false,
51
        },
52
        dashboardColumn: {
53
            type: String,
54
        },
55
        id: {
56
            type: String,
57
            required: true,
58
        },
59
        name: {
60
            type: String,
61
            required: true,
62
        },
63
        icon: {
64
            type: String,
65
        },
66
        description: {
67
            type: String,
68
            required: true,
69
        },
70
        loading: {
71
            type: Boolean,
72
        },
73
        settings: {
74
            type: Object,
75
        },
76
        settings_definitions: {
77
            type: Array,
78
        },
79
    },
80
    setup(props) {
81
        // Destructure props so they are all reactive refs
82
        const {
83
            display,
84
            alreadyAdded,
85
            dashboardColumn,
86
            id,
87
            name,
88
            icon,
89
            description,
90
            loading,
91
            settings,
92
            settings_definitions,
93
        } = toRefs(props);
94
95
        const addWidget = inject("addWidget");
96
        const removeWidget = inject("removeWidget");
97
        const moveWidget = inject("moveWidget");
98
99
        const handleAddWidget = () => {
100
            if (addWidget) addWidget();
101
        };
102
103
        const handleRemoveWidget = () => {
104
            if (removeWidget) removeWidget();
105
        };
106
107
        const handleMoveWidget = () => {
108
            if (moveWidget) moveWidget();
109
        };
110
111
        return {
112
            display,
113
            alreadyAdded,
114
            dashboardColumn,
115
            id,
116
            name,
117
            icon,
118
            description,
119
            loading,
120
            settings,
121
            settings_definitions,
122
            handleAddWidget,
123
            handleRemoveWidget,
124
            handleMoveWidget,
125
        };
126
    },
127
};
128
</script>
129
130
<style scoped>
131
.widget-title {
132
    margin-top: 0;
133
    font-weight: bold;
134
    font-size: 1.2rem;
135
}
136
</style>
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/composables/base-widget.js (-1 / +97 lines)
Line 0 Link Here
0
- 
1
// composables/useBaseWidget.js
2
import { ref, computed, watch, provide, onMounted, onBeforeMount } from "vue";
3
import VueCookies from "vue-cookies";
4
5
export function useBaseWidget(widgetConfig, emit) {
6
    const loading = ref(
7
        widgetConfig.loading !== undefined ? widgetConfig.loading : true
8
    );
9
    const settings = ref(widgetConfig.settings || {});
10
    const settings_definitions = ref(widgetConfig.settings_definitions || []);
11
    const removeWidget = () => emit("removed", widgetConfig);
12
    const addWidget = () => emit("added", widgetConfig);
13
    const moveWidget = () => {
14
        emit("moveWidget", {
15
            componentName: widgetConfig.id,
16
            currentColumn: widgetConfig.dashboardColumn,
17
        });
18
    };
19
20
    provide("removeWidget", removeWidget);
21
    provide("addWidget", addWidget);
22
    provide("moveWidget", moveWidget);
23
24
    const widgetWrapperProps = computed(() => ({
25
        id: widgetConfig.id,
26
        display: widgetConfig.display,
27
        alreadyAdded: widgetConfig.alreadyAdded,
28
        dashboardColumn: widgetConfig.dashboardColumn,
29
        name: widgetConfig.name,
30
        icon: widgetConfig.icon,
31
        loading: loading.value,
32
        description: widgetConfig.description,
33
        settings: settings.value,
34
        settings_definitions: settings_definitions.value,
35
    }));
36
37
    const getWidgetSavedSettings = () => {
38
        const cookie = VueCookies.get(
39
            "widget-" + widgetConfig.id + "-settings"
40
        );
41
        return cookie || null;
42
    };
43
44
    onBeforeMount(() => {
45
        if (widgetConfig.display === "dashboard") {
46
            const cookie = getWidgetSavedSettings();
47
            if (cookie) {
48
                settings.value = cookie;
49
            }
50
        }
51
    });
52
53
    /**
54
     * Lifecycle method that runs when the widget is mounted on the dashboard.
55
     * Use this to e.g. fetch data
56
     *
57
     * Sample usage from ERMCounts.vue:
58
     *
59
     *   baseWidget.onDashboardMounted(() => {
60
     *     getCounts();
61
     *   });
62
     */
63
    function onDashboardMounted(callback) {
64
        onMounted(() => {
65
            if (widgetConfig.display === "dashboard") {
66
                callback?.();
67
            }
68
        });
69
    }
70
71
    watch(
72
        settings,
73
        newSettings => {
74
            if (widgetConfig.display === "dashboard" && newSettings !== null) {
75
                VueCookies.set(
76
                    "widget-" + widgetConfig.id + "-settings",
77
                    JSON.stringify(newSettings),
78
                    "30d"
79
                );
80
            }
81
        },
82
        { deep: true }
83
    );
84
85
    return {
86
        ...widgetConfig,
87
        loading,
88
        display: widgetConfig.display,
89
        settings,
90
        settings_definitions,
91
        widgetWrapperProps,
92
        removeWidget,
93
        addWidget,
94
        moveWidget,
95
        onDashboardMounted,
96
    };
97
}

Return to bug 39320