inicontent 1.0.2 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +203 -21
- package/NOTICE +11 -0
- package/README.md +4 -0
- package/app/app.vue +0 -16
- package/app/assets/main.css +2 -1
- package/app/components/Dashboard/Grid.vue +6 -3
- package/app/components/Dashboard/View.vue +551 -80
- package/app/components/Dashboard/Widget/Table.vue +3 -1
- package/app/components/Form/Drawer.vue +4 -1
- package/app/components/Header.vue +1 -1
- package/app/components/Table/TranslateDrawer.vue +1 -1
- package/app/composables/databaseCookies.ts +1 -55
- package/app/composables/openDrawer.ts +3 -2
- package/app/composables/useDashboardData.ts +31 -0
- package/app/middleware/database.ts +0 -1
- package/app/pages/[[database]]/admin/dashboards/[id]/index.vue +168 -18
- package/app/pages/[[database]]/admin/tables/[table]/[id]/index.vue +1 -1
- package/package.json +2 -1
- package/app/components/Dashboard/Editor.vue +0 -221
- package/app/pages/[[database]]/admin/dashboards/[id]/edit.vue +0 -66
|
@@ -98,7 +98,10 @@ defineSlots<{
|
|
|
98
98
|
}>();
|
|
99
99
|
|
|
100
100
|
const Drawers = useState<DrawerRef>("drawers", () => []);
|
|
101
|
-
const defaultWidth = useCookie<number | string>("
|
|
101
|
+
const defaultWidth = useCookie<number | string>("drawerWidth", {
|
|
102
|
+
sameSite: true,
|
|
103
|
+
default: () => 560,
|
|
104
|
+
});
|
|
102
105
|
const formRefs = ref<FormRef[]>([]);
|
|
103
106
|
|
|
104
107
|
const screenHalf = window.screen.width / 2;
|
|
@@ -200,7 +200,7 @@ const userDropdownOptions = computed(() => [
|
|
|
200
200
|
show: user.value?.role === config.public.idOne,
|
|
201
201
|
},
|
|
202
202
|
{
|
|
203
|
-
label: () => h(NFlex,{ justify: "start", align: "center", flexDirection: Language.value === "ar" ? 'row-reverse' : 'row' }, () => [
|
|
203
|
+
label: () => h(NFlex,{ justify: Language.value === "ar" ? "end" : "start", align: "center", style: { flexDirection: Language.value === "ar" ? 'row-reverse' : 'row' } }, () => [
|
|
204
204
|
t("billing"),
|
|
205
205
|
h(NIcon, { color: ThemeConfig.value.primaryColor}, () => h(Icon, { name: "tabler:external-link" }))
|
|
206
206
|
]),
|
|
@@ -95,7 +95,7 @@ const Language = useScopedCookie<LanguagesType>("language", database.value?.slug
|
|
|
95
95
|
const sessionID = useScopedCookie<string>("sid", database.value?.slug);
|
|
96
96
|
const table = useState<Table>("table");
|
|
97
97
|
|
|
98
|
-
const drawerWidth = useCookie<number | string>("
|
|
98
|
+
const drawerWidth = useCookie<number | string>("drawerWidth", {
|
|
99
99
|
sameSite: true,
|
|
100
100
|
default: () => 560,
|
|
101
101
|
});
|
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
const syncedCookieKeys = ["language", "theme", "sid", "redirectTo"] as const;
|
|
2
|
-
// Only theme is shared globally across all databases. language, sid and
|
|
3
|
-
// redirectTo are scoped per database so each database keeps its own value.
|
|
4
|
-
const globalOnlyKeys = new Set(["theme"]);
|
|
5
|
-
|
|
6
|
-
function isSet(value: string | null | undefined) {
|
|
7
|
-
return value !== null && value !== undefined && value !== "";
|
|
8
|
-
}
|
|
9
|
-
|
|
10
1
|
function resolveDatabaseSlug(databaseSlug?: string) {
|
|
11
2
|
if (databaseSlug) return databaseSlug;
|
|
12
3
|
|
|
@@ -31,49 +22,4 @@ export function useScopedCookie<T>(key: string, databaseSlug?: string) {
|
|
|
31
22
|
`${resolveDatabaseSlug(databaseSlug)}_${key}`,
|
|
32
23
|
{ sameSite: true },
|
|
33
24
|
);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function syncCookiesFromDatabase(databaseSlug?: string) {
|
|
37
|
-
for (const key of syncedCookieKeys) {
|
|
38
|
-
const globalCookie = useCookie<string | null>(key, { sameSite: true });
|
|
39
|
-
const scopedCookie = useCookie<string | null>(
|
|
40
|
-
`${resolveDatabaseSlug(databaseSlug)}_${key}`,
|
|
41
|
-
{
|
|
42
|
-
sameSite: true,
|
|
43
|
-
},
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
if (isSet(scopedCookie.value)) {
|
|
47
|
-
// Always keep global in sync so components reading useCookie(key) work.
|
|
48
|
-
if (!globalOnlyKeys.has(key)) {
|
|
49
|
-
globalCookie.value = null;
|
|
50
|
-
} else {
|
|
51
|
-
globalCookie.value = scopedCookie.value;
|
|
52
|
-
}
|
|
53
|
-
} else if (isSet(globalCookie.value)) {
|
|
54
|
-
scopedCookie.value = globalCookie.value;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function syncCookiesToDatabase(databaseSlug?: string) {
|
|
60
|
-
for (const key of syncedCookieKeys) {
|
|
61
|
-
const globalCookie = useCookie<string | null>(key, { sameSite: true });
|
|
62
|
-
const scopedCookie = useCookie<string | null>(
|
|
63
|
-
`${resolveDatabaseSlug(databaseSlug)}_${key}`,
|
|
64
|
-
{
|
|
65
|
-
sameSite: true,
|
|
66
|
-
},
|
|
67
|
-
);
|
|
68
|
-
|
|
69
|
-
if (isSet(globalCookie.value)) {
|
|
70
|
-
scopedCookie.value = globalCookie.value;
|
|
71
|
-
// For sid, wipe the global so the token is only in the scoped cookie.
|
|
72
|
-
if (!globalOnlyKeys.has(key)) {
|
|
73
|
-
globalCookie.value = null;
|
|
74
|
-
}
|
|
75
|
-
} else if (isSet(scopedCookie.value) && globalOnlyKeys.has(key)) {
|
|
76
|
-
globalCookie.value = scopedCookie.value;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
25
|
+
}
|
|
@@ -68,14 +68,15 @@ export default function (
|
|
|
68
68
|
mode: "view" | "edit" = "edit",
|
|
69
69
|
) {
|
|
70
70
|
const Drawers = useState<DrawerRef>("drawers", () => []);
|
|
71
|
-
const defaultWidth = useCookie<number | string>("
|
|
71
|
+
const defaultWidth = useCookie<number | string>("drawerWidth", {
|
|
72
72
|
sameSite: true,
|
|
73
|
+
default: () => 560,
|
|
73
74
|
});
|
|
74
75
|
const index = Drawers.value.findIndex(
|
|
75
76
|
(d) => d.table === table && d.id === id,
|
|
76
77
|
);
|
|
77
78
|
|
|
78
|
-
let width = defaultWidth.value ??
|
|
79
|
+
let width = defaultWidth.value ?? 560;
|
|
79
80
|
if (index === -1) {
|
|
80
81
|
if (Drawers.value.length) {
|
|
81
82
|
for (let index = 0; index < Drawers.value.length; index++) {
|
|
@@ -306,5 +306,36 @@ export function useDashboardData(
|
|
|
306
306
|
watch(dateRangeOverride, () => refresh());
|
|
307
307
|
}
|
|
308
308
|
|
|
309
|
+
// Re-fetch when the widget's own criteria change (source table, type,
|
|
310
|
+
// operation, field, filters, sort, …) so inline edits preview live
|
|
311
|
+
// before the dashboard is saved. A short debounce absorbs rapid
|
|
312
|
+
// changes while typing in the widget editor.
|
|
313
|
+
const widgetSignature = computed(() =>
|
|
314
|
+
JSON.stringify({
|
|
315
|
+
type: widget.type,
|
|
316
|
+
table: widget.table,
|
|
317
|
+
operation: widget.operation,
|
|
318
|
+
field: widget.field,
|
|
319
|
+
groupBy: widget.groupBy,
|
|
320
|
+
dateField: widget.dateField,
|
|
321
|
+
dateRange: widget.dateRange,
|
|
322
|
+
searchArray: widget.searchArray,
|
|
323
|
+
sortField: widget.sortField,
|
|
324
|
+
sortOrder: widget.sortOrder,
|
|
325
|
+
columns: widget.columns,
|
|
326
|
+
limit: widget.limit,
|
|
327
|
+
}),
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
let refreshTimer: ReturnType<typeof setTimeout> | null = null;
|
|
331
|
+
watch(widgetSignature, () => {
|
|
332
|
+
if (refreshTimer) clearTimeout(refreshTimer);
|
|
333
|
+
refreshTimer = setTimeout(() => refresh(), 250);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
onBeforeUnmount(() => {
|
|
337
|
+
if (refreshTimer) clearTimeout(refreshTimer);
|
|
338
|
+
});
|
|
339
|
+
|
|
309
340
|
return { value, data, total, groups, timeSeries, loading, refresh };
|
|
310
341
|
}
|
|
@@ -1,6 +1,53 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<NSpin :show="loading">
|
|
3
|
-
<NCard
|
|
3
|
+
<NCard style="background: none" :bordered="false">
|
|
4
|
+
<template #header>
|
|
5
|
+
<NFlex align="center" :size="8">
|
|
6
|
+
<template v-if="editMode && draft">
|
|
7
|
+
<NPopover trigger="click" placement="bottom-start">
|
|
8
|
+
<template #trigger>
|
|
9
|
+
<NButton circle quaternary size="small">
|
|
10
|
+
<NIcon size="20">
|
|
11
|
+
<Icon
|
|
12
|
+
:name="
|
|
13
|
+
draft.icon
|
|
14
|
+
? `tabler:${draft.icon}`
|
|
15
|
+
: 'tabler:chart-bar'
|
|
16
|
+
"
|
|
17
|
+
/>
|
|
18
|
+
</NIcon>
|
|
19
|
+
</NButton>
|
|
20
|
+
</template>
|
|
21
|
+
<div style="width: 280px">
|
|
22
|
+
<LazyFieldIcon
|
|
23
|
+
v-model="draft.icon"
|
|
24
|
+
:field="{ key: 'icon', type: 'string', subType: 'icon' }"
|
|
25
|
+
/>
|
|
26
|
+
</div>
|
|
27
|
+
</NPopover>
|
|
28
|
+
<NInput
|
|
29
|
+
v-model:value="draft.name"
|
|
30
|
+
size="small"
|
|
31
|
+
:placeholder="t('dashboardName')"
|
|
32
|
+
style="max-width: 360px"
|
|
33
|
+
/>
|
|
34
|
+
</template>
|
|
35
|
+
<template v-else>
|
|
36
|
+
<NIconWrapper :border-radius="50" style="font-style: normal">
|
|
37
|
+
<NIcon size="20">
|
|
38
|
+
<Icon
|
|
39
|
+
:name="
|
|
40
|
+
dashboard?.icon
|
|
41
|
+
? `tabler:${dashboard.icon}`
|
|
42
|
+
: 'tabler:chart-bar'
|
|
43
|
+
"
|
|
44
|
+
/>
|
|
45
|
+
</NIcon>
|
|
46
|
+
</NIconWrapper>
|
|
47
|
+
<NH4 style="margin: 0">{{ dashboard?.name ?? t("dashboard") }}</NH4>
|
|
48
|
+
</template>
|
|
49
|
+
</NFlex>
|
|
50
|
+
</template>
|
|
4
51
|
<template #header-extra>
|
|
5
52
|
<NFlex :size="8" align="center">
|
|
6
53
|
<NPopover v-if="dashboard?.widgets?.length" trigger="click" placement="bottom-end">
|
|
@@ -22,12 +69,28 @@
|
|
|
22
69
|
style="width: 200px"
|
|
23
70
|
/>
|
|
24
71
|
</NPopover>
|
|
72
|
+
<template v-if="editMode && draft">
|
|
73
|
+
<NButton size="small" secondary @click="addWidget">
|
|
74
|
+
<template #icon>
|
|
75
|
+
<NIcon>
|
|
76
|
+
<Icon name="tabler:plus" />
|
|
77
|
+
</NIcon>
|
|
78
|
+
</template>
|
|
79
|
+
{{ t("addWidget") }}
|
|
80
|
+
</NButton>
|
|
81
|
+
<NButton size="small" secondary @click="cancelEdit">
|
|
82
|
+
{{ t("cancel") }}
|
|
83
|
+
</NButton>
|
|
84
|
+
<NButton size="small" type="primary" secondary :loading="saving" @click="save">
|
|
85
|
+
{{ t("save") }}
|
|
86
|
+
</NButton>
|
|
87
|
+
</template>
|
|
25
88
|
<NButton
|
|
26
|
-
v-if="user?.role === config.public.idOne"
|
|
89
|
+
v-else-if="user?.role === config.public.idOne"
|
|
27
90
|
size="small"
|
|
28
91
|
type="primary"
|
|
29
92
|
secondary
|
|
30
|
-
@click="
|
|
93
|
+
@click="enterEditMode"
|
|
31
94
|
>
|
|
32
95
|
<template #icon>
|
|
33
96
|
<NIcon>
|
|
@@ -40,15 +103,20 @@
|
|
|
40
103
|
</template>
|
|
41
104
|
<LazyDashboardView
|
|
42
105
|
v-if="dashboard"
|
|
43
|
-
|
|
106
|
+
ref="viewRef"
|
|
107
|
+
:dashboard="editMode && draft ? draft : dashboard"
|
|
44
108
|
:database-slug="databaseSlug"
|
|
45
109
|
:date-range-override="dateRangeOverride"
|
|
110
|
+
:edit-mode="editMode && !!draft"
|
|
111
|
+
v-model:widgets="widgetsModel"
|
|
46
112
|
/>
|
|
47
113
|
</NCard>
|
|
48
114
|
</NSpin>
|
|
49
115
|
</template>
|
|
50
116
|
|
|
51
117
|
<script setup lang="ts">
|
|
118
|
+
import type { DashboardView } from "#components";
|
|
119
|
+
|
|
52
120
|
definePageMeta({
|
|
53
121
|
layout: "table",
|
|
54
122
|
middleware: ["database", "user", "dashboard", "global"],
|
|
@@ -59,22 +127,35 @@ const config = useRuntimeConfig();
|
|
|
59
127
|
const user = useState<User>("user");
|
|
60
128
|
const database = useState<Database>("database");
|
|
61
129
|
|
|
62
|
-
const databaseSlug = computed(
|
|
63
|
-
(route.params.database as string) || database.value?.slug || "",
|
|
130
|
+
const databaseSlug = computed(
|
|
131
|
+
() => (route.params.database as string) || database.value?.slug || "",
|
|
64
132
|
);
|
|
65
133
|
|
|
66
|
-
const
|
|
67
|
-
|
|
134
|
+
const Language = useScopedCookie<LanguagesType>(
|
|
135
|
+
"language",
|
|
136
|
+
database.value?.slug,
|
|
68
137
|
);
|
|
69
|
-
|
|
70
|
-
const Language = useScopedCookie<LanguagesType>("language", database.value?.slug);
|
|
71
138
|
const sessionID = useScopedCookie<string>("sid", database.value?.slug);
|
|
72
139
|
|
|
73
140
|
const dashboard = ref<Dashboard | null>(null);
|
|
74
|
-
const currentDashboard = useState<Dashboard | null>(
|
|
141
|
+
const currentDashboard = useState<Dashboard | null>(
|
|
142
|
+
"currentDashboard",
|
|
143
|
+
() => null,
|
|
144
|
+
);
|
|
75
145
|
const loading = ref(true);
|
|
76
146
|
const dateRangeOverride = ref<WidgetDateRange | undefined>(undefined);
|
|
77
147
|
|
|
148
|
+
const editMode = ref(false);
|
|
149
|
+
const draft = ref<Dashboard | null>(null);
|
|
150
|
+
const saving = ref(false);
|
|
151
|
+
|
|
152
|
+
const widgetsModel = computed({
|
|
153
|
+
get: () => draft.value?.widgets ?? [],
|
|
154
|
+
set: (v) => {
|
|
155
|
+
if (draft.value) draft.value.widgets = v;
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
|
|
78
159
|
const dateRangeOptions = [
|
|
79
160
|
{ label: t("last7Days"), value: "7d" },
|
|
80
161
|
{ label: t("last30Days"), value: "30d" },
|
|
@@ -83,6 +164,76 @@ const dateRangeOptions = [
|
|
|
83
164
|
{ label: t("allTime"), value: "all" },
|
|
84
165
|
];
|
|
85
166
|
|
|
167
|
+
const canEdit = computed(() => user.value?.role === config.public.idOne);
|
|
168
|
+
|
|
169
|
+
const viewRef = ref<InstanceType<typeof DashboardView> | null>(null);
|
|
170
|
+
|
|
171
|
+
function addWidget() {
|
|
172
|
+
viewRef.value?.addWidget();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const parseWidgets = (d: Dashboard) => {
|
|
176
|
+
if (typeof d.widgets === "string") {
|
|
177
|
+
try {
|
|
178
|
+
d.widgets = JSON.parse(d.widgets);
|
|
179
|
+
} catch {}
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
function enterEditMode() {
|
|
184
|
+
if (!dashboard.value || editingNow.value) return;
|
|
185
|
+
draft.value = deepClone(dashboard.value) ?? null;
|
|
186
|
+
if (draft.value && !draft.value.widgets) draft.value.widgets = [];
|
|
187
|
+
editMode.value = true;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function cancelEdit() {
|
|
191
|
+
editMode.value = false;
|
|
192
|
+
draft.value = null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const editingNow = computed(() => editMode.value && !!draft.value);
|
|
196
|
+
|
|
197
|
+
async function save() {
|
|
198
|
+
if (!draft.value?.name) {
|
|
199
|
+
window.$message.error(t("inputsAreInvalid"));
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
saving.value = true;
|
|
203
|
+
try {
|
|
204
|
+
const slug = databaseSlug.value ? `${databaseSlug.value}/` : "";
|
|
205
|
+
const res = await $fetch<apiResponse<Dashboard>>(
|
|
206
|
+
`${config.public.apiBase}${slug}dashboards/${route.params.id}`,
|
|
207
|
+
{
|
|
208
|
+
method: "PUT",
|
|
209
|
+
body: {
|
|
210
|
+
name: draft.value.name,
|
|
211
|
+
description: draft.value.description,
|
|
212
|
+
icon: draft.value.icon,
|
|
213
|
+
widgets: draft.value.widgets,
|
|
214
|
+
},
|
|
215
|
+
params: {
|
|
216
|
+
locale: Language.value,
|
|
217
|
+
[`${databaseSlug.value}_sid`]: sessionID.value,
|
|
218
|
+
},
|
|
219
|
+
credentials: "include",
|
|
220
|
+
},
|
|
221
|
+
);
|
|
222
|
+
if (res.result) {
|
|
223
|
+
dashboard.value = res.result;
|
|
224
|
+
currentDashboard.value = res.result;
|
|
225
|
+
parseWidgets(dashboard.value);
|
|
226
|
+
window.$message.success(res.message);
|
|
227
|
+
cancelEdit();
|
|
228
|
+
} else {
|
|
229
|
+
window.$message.error(res.message ?? t("error"));
|
|
230
|
+
}
|
|
231
|
+
} catch {
|
|
232
|
+
window.$message.error(t("error"));
|
|
233
|
+
}
|
|
234
|
+
saving.value = false;
|
|
235
|
+
}
|
|
236
|
+
|
|
86
237
|
onMounted(async () => {
|
|
87
238
|
try {
|
|
88
239
|
const slug = databaseSlug.value ? `${databaseSlug.value}/` : "";
|
|
@@ -99,11 +250,8 @@ onMounted(async () => {
|
|
|
99
250
|
dashboard.value = res.result;
|
|
100
251
|
currentDashboard.value = res.result;
|
|
101
252
|
if (dashboard.value) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
dashboard.value.widgets = JSON.parse(dashboard.value.widgets);
|
|
105
|
-
} catch {}
|
|
106
|
-
}
|
|
253
|
+
parseWidgets(dashboard.value);
|
|
254
|
+
if (route.query.edit === "true" && canEdit.value) enterEditMode();
|
|
107
255
|
}
|
|
108
256
|
} catch {
|
|
109
257
|
window.$message.error(t("error"));
|
|
@@ -112,6 +260,8 @@ onMounted(async () => {
|
|
|
112
260
|
});
|
|
113
261
|
|
|
114
262
|
useHead({
|
|
115
|
-
title: computed(
|
|
263
|
+
title: computed(
|
|
264
|
+
() => `${dashboard.value?.name ?? t("dashboard")} | ${t("dashboards")}`,
|
|
265
|
+
),
|
|
116
266
|
});
|
|
117
|
-
</script>
|
|
267
|
+
</script>
|
package/package.json
CHANGED
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<NFlex vertical :size="24">
|
|
3
|
-
<NCard :title="t('dashboardSettings')">
|
|
4
|
-
<NForm label-placement="left" label-width="120">
|
|
5
|
-
<NFormItem :label="t('name')">
|
|
6
|
-
<NInput v-model:value="model.name" :placeholder="t('dashboardName')" />
|
|
7
|
-
</NFormItem>
|
|
8
|
-
<NFormItem :label="t('description')">
|
|
9
|
-
<NInput
|
|
10
|
-
v-model:value="model.description"
|
|
11
|
-
type="textarea"
|
|
12
|
-
:placeholder="t('description')"
|
|
13
|
-
:rows="2"
|
|
14
|
-
/>
|
|
15
|
-
</NFormItem>
|
|
16
|
-
<LazyFieldIcon v-model="model.icon" :field="{ key: 'icon', type: 'string', subType: 'icon' }" />
|
|
17
|
-
</NForm>
|
|
18
|
-
</NCard>
|
|
19
|
-
|
|
20
|
-
<NCard :title="t('widgets')">
|
|
21
|
-
<template #header-extra>
|
|
22
|
-
<NButton size="small" @click="addWidget" type="primary" secondary>
|
|
23
|
-
<template #icon>
|
|
24
|
-
<NIcon>
|
|
25
|
-
<Icon name="tabler:plus" />
|
|
26
|
-
</NIcon>
|
|
27
|
-
</template>
|
|
28
|
-
{{ t("addWidget") }}
|
|
29
|
-
</NButton>
|
|
30
|
-
</template>
|
|
31
|
-
|
|
32
|
-
<VueDraggable
|
|
33
|
-
:model-value="model.widgets as any[]"
|
|
34
|
-
item-key="id"
|
|
35
|
-
handle=".drag-handle"
|
|
36
|
-
:animation="200"
|
|
37
|
-
>
|
|
38
|
-
<template v-for="(widget,index) in model.widgets">
|
|
39
|
-
<NCard
|
|
40
|
-
size="small"
|
|
41
|
-
:style="{ marginBottom: '12px' }"
|
|
42
|
-
:title="widget.title || t('untitledWidget')"
|
|
43
|
-
>
|
|
44
|
-
<template #header-extra>
|
|
45
|
-
<NFlex :size="4">
|
|
46
|
-
<span
|
|
47
|
-
class="drag-handle"
|
|
48
|
-
style="cursor: grab; display: inline-flex; align-items: center; padding: 4px"
|
|
49
|
-
>
|
|
50
|
-
<NIcon :size="16">
|
|
51
|
-
<Icon name="tabler:grip-vertical" />
|
|
52
|
-
</NIcon>
|
|
53
|
-
</span>
|
|
54
|
-
<NButton
|
|
55
|
-
size="tiny"
|
|
56
|
-
quaternary
|
|
57
|
-
@click="editingWidgetIndex = index"
|
|
58
|
-
>
|
|
59
|
-
<template #icon>
|
|
60
|
-
<NIcon>
|
|
61
|
-
<Icon name="tabler:edit" />
|
|
62
|
-
</NIcon>
|
|
63
|
-
</template>
|
|
64
|
-
</NButton>
|
|
65
|
-
<NButton
|
|
66
|
-
size="tiny"
|
|
67
|
-
quaternary
|
|
68
|
-
type="error"
|
|
69
|
-
@click="removeWidget(index)"
|
|
70
|
-
>
|
|
71
|
-
<template #icon>
|
|
72
|
-
<NIcon>
|
|
73
|
-
<Icon name="tabler:trash" />
|
|
74
|
-
</NIcon>
|
|
75
|
-
</template>
|
|
76
|
-
</NButton>
|
|
77
|
-
</NFlex>
|
|
78
|
-
</template>
|
|
79
|
-
<NFlex :size="8">
|
|
80
|
-
<NTag size="small" :bordered="false" type="info">{{
|
|
81
|
-
t(widget.type)
|
|
82
|
-
}}</NTag>
|
|
83
|
-
<NTag size="small" :bordered="false">{{ widget.table || "—" }}</NTag>
|
|
84
|
-
<NTag v-if="widget.size" size="small" :bordered="false" type="warning">{{
|
|
85
|
-
t(widget.size)
|
|
86
|
-
}}</NTag>
|
|
87
|
-
</NFlex>
|
|
88
|
-
</NCard>
|
|
89
|
-
</template>
|
|
90
|
-
</VueDraggable>
|
|
91
|
-
|
|
92
|
-
<NEmpty v-if="!model.widgets?.length" :description="t('noWidgets')" />
|
|
93
|
-
</NCard>
|
|
94
|
-
|
|
95
|
-
<NFlex justify="end">
|
|
96
|
-
<NButton @click="$router.back()">{{ t("cancel") }}</NButton>
|
|
97
|
-
<NButton type="primary" :loading="saving" @click="save">
|
|
98
|
-
{{ t("save") }}
|
|
99
|
-
</NButton>
|
|
100
|
-
</NFlex>
|
|
101
|
-
|
|
102
|
-
<NDrawer
|
|
103
|
-
v-model:show="showWidgetDrawer"
|
|
104
|
-
:width="420"
|
|
105
|
-
placement="right"
|
|
106
|
-
>
|
|
107
|
-
<NDrawerContent :title="t('editWidget')" closable>
|
|
108
|
-
<DashboardWidgetEditor
|
|
109
|
-
v-if="editingWidget"
|
|
110
|
-
v-model="editingWidget"
|
|
111
|
-
/>
|
|
112
|
-
<template #footer>
|
|
113
|
-
<NButton type="primary" @click="showWidgetDrawer = false">
|
|
114
|
-
{{ t("confirm") }}
|
|
115
|
-
</NButton>
|
|
116
|
-
</template>
|
|
117
|
-
</NDrawerContent>
|
|
118
|
-
</NDrawer>
|
|
119
|
-
</NFlex>
|
|
120
|
-
</template>
|
|
121
|
-
|
|
122
|
-
<script lang="ts" setup>
|
|
123
|
-
import { VueDraggable } from "vue-draggable-plus";
|
|
124
|
-
|
|
125
|
-
const props = defineProps<{
|
|
126
|
-
dashboard: Dashboard;
|
|
127
|
-
}>();
|
|
128
|
-
|
|
129
|
-
const emit = defineEmits<{
|
|
130
|
-
saved: [dashboard: Dashboard];
|
|
131
|
-
}>();
|
|
132
|
-
|
|
133
|
-
const database = useState<Database>("database");
|
|
134
|
-
const config = useRuntimeConfig();
|
|
135
|
-
const Language = useScopedCookie<LanguagesType>("language", database.value?.slug);
|
|
136
|
-
const sessionID = useScopedCookie<string>("sid", database.value?.slug);
|
|
137
|
-
|
|
138
|
-
const model = ref<Dashboard>({ ...props.dashboard, widgets: [...(props.dashboard.widgets ?? [])] });
|
|
139
|
-
const saving = ref(false);
|
|
140
|
-
const editingWidgetIndex = ref<number | null>(null);
|
|
141
|
-
const showWidgetDrawer = ref(false);
|
|
142
|
-
|
|
143
|
-
const editingWidget = computed({
|
|
144
|
-
get: () =>
|
|
145
|
-
editingWidgetIndex.value !== null
|
|
146
|
-
? model.value.widgets?.[editingWidgetIndex.value] ?? null
|
|
147
|
-
: null,
|
|
148
|
-
set: (v) => {
|
|
149
|
-
if (editingWidgetIndex.value !== null && v && model.value.widgets) {
|
|
150
|
-
model.value.widgets[editingWidgetIndex.value] = v;
|
|
151
|
-
}
|
|
152
|
-
},
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
watch(editingWidgetIndex, (v) => {
|
|
156
|
-
showWidgetDrawer.value = v !== null;
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
watch(showWidgetDrawer, (v) => {
|
|
160
|
-
if (!v) editingWidgetIndex.value = null;
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
function addWidget() {
|
|
164
|
-
if (!model.value.widgets) model.value.widgets = [];
|
|
165
|
-
const newWidget: Widget = {
|
|
166
|
-
id: `w_${Date.now()}`,
|
|
167
|
-
type: "counter",
|
|
168
|
-
title: "",
|
|
169
|
-
table: "",
|
|
170
|
-
operation: "count",
|
|
171
|
-
size: "small",
|
|
172
|
-
dateRange: "all",
|
|
173
|
-
searchArray: { and: [[null, "=", null]] },
|
|
174
|
-
};
|
|
175
|
-
model.value.widgets.push(newWidget);
|
|
176
|
-
editingWidgetIndex.value = model.value.widgets.length - 1;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function removeWidget(index: number) {
|
|
180
|
-
model.value.widgets?.splice(index, 1);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
async function save() {
|
|
184
|
-
if (!model.value.name) {
|
|
185
|
-
window.$message.error(t("inputsAreInvalid"));
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
saving.value = true;
|
|
189
|
-
try {
|
|
190
|
-
const isUpdate = !!props.dashboard.id;
|
|
191
|
-
const url = isUpdate
|
|
192
|
-
? `${config.public.apiBase}${database.value.slug}/dashboards/${props.dashboard.id}`
|
|
193
|
-
: `${config.public.apiBase}${database.value.slug}/dashboards`;
|
|
194
|
-
|
|
195
|
-
const res = await $fetch<apiResponse<Dashboard>>(url, {
|
|
196
|
-
method: isUpdate ? "PUT" : "POST",
|
|
197
|
-
body: {
|
|
198
|
-
name: model.value.name,
|
|
199
|
-
description: model.value.description,
|
|
200
|
-
icon: model.value.icon,
|
|
201
|
-
widgets: model.value.widgets,
|
|
202
|
-
},
|
|
203
|
-
params: {
|
|
204
|
-
locale: Language.value,
|
|
205
|
-
[`${database.value.slug}_sid`]: sessionID.value,
|
|
206
|
-
},
|
|
207
|
-
credentials: "include",
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
if (res.result) {
|
|
211
|
-
window.$message.success(res.message);
|
|
212
|
-
emit("saved", res.result);
|
|
213
|
-
} else {
|
|
214
|
-
window.$message.error(res.message ?? t("error"));
|
|
215
|
-
}
|
|
216
|
-
} catch {
|
|
217
|
-
window.$message.error(t("error"));
|
|
218
|
-
}
|
|
219
|
-
saving.value = false;
|
|
220
|
-
}
|
|
221
|
-
</script>
|