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.
@@ -98,7 +98,10 @@ defineSlots<{
98
98
  }>();
99
99
 
100
100
  const Drawers = useState<DrawerRef>("drawers", () => []);
101
- const defaultWidth = useCookie<number | string>("width");
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>("translateDrawerWidth", {
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>("width", {
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 ?? 251;
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
  }
@@ -70,7 +70,6 @@ export default defineNuxtRouteMiddleware(async (to) => {
70
70
  }
71
71
 
72
72
  formatDatabase();
73
- syncCookiesFromDatabase(database.value.slug);
74
73
 
75
74
  useState<ThemeConfig>("ThemeConfig", () => ({
76
75
  primaryColor: "#FF9800",
@@ -1,6 +1,53 @@
1
1
  <template>
2
2
  <NSpin :show="loading">
3
- <NCard :title="dashboard?.name ?? t('dashboard')" style="background:none" :bordered="false">
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="navigateTo(`${routeBase}/admin/dashboards/${route.params.id}/edit`)"
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
- :dashboard="dashboard"
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 routeBase = computed(() =>
67
- route.params.database ? `/${route.params.database}` : "",
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>("currentDashboard", () => 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
- if (typeof dashboard.value.widgets === "string") {
103
- try {
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(() => `${dashboard.value?.name ?? t("dashboard")} | ${t("dashboards")}`),
263
+ title: computed(
264
+ () => `${dashboard.value?.name ?? t("dashboard")} | ${t("dashboards")}`,
265
+ ),
116
266
  });
117
- </script>
267
+ </script>
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <NFlex vertical>
3
- <LazyFormDrawer v-if="!$device.isMobile" />
3
+ <LazyFormDrawer/>
4
4
  <NCard style="height: fit-content">
5
5
  <template #header>
6
6
  <NPerformantEllipsis>{{ t(table.slug) }}: {{ itemLabel }}</NPerformantEllipsis>
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "inicontent",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
+ "license": "Apache-2.0",
4
5
  "type": "module",
5
6
  "main": "./nuxt.config.ts",
6
7
  "dependencies": {
@@ -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>