@lobb-js/lobb-ext-reports 0.7.0 → 0.7.2

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.
@@ -8,24 +8,31 @@
8
8
  interface Props {
9
9
  column?: number;
10
10
  cellHeight?: string;
11
- editMode?: boolean;
12
- currentLayout?: LayoutItem[];
11
+ editable?: boolean;
12
+ onLayoutChange?: (layout: LayoutItem[]) => void;
13
13
  children: Snippet;
14
14
  }
15
15
 
16
16
  let {
17
17
  column = 12,
18
18
  cellHeight = "150px",
19
- editMode = $bindable(false),
20
- currentLayout = $bindable([]),
19
+ editable = $bindable(false),
20
+ onLayoutChange,
21
21
  children,
22
22
  }: Props = $props();
23
23
 
24
24
  let gridEl: HTMLElement | undefined = $state();
25
25
  let grid: GridStack | null = null;
26
- let initialized = false;
26
+ let initialized = $state(false);
27
27
  let resizeObserver: ResizeObserver | null = null;
28
28
  let lastWidth = 0;
29
+ let containerWidth = $state(0);
30
+ let saveTimeout: ReturnType<typeof setTimeout> | null = null;
31
+
32
+ let editMode = $derived(containerWidth >= 1200);
33
+
34
+ // Keep editable in sync so parent can read it
35
+ $effect(() => { editable = editMode; });
29
36
 
30
37
  function getResponsiveColumns(width: number): number {
31
38
  if (width >= 1200) return 12;
@@ -51,8 +58,9 @@
51
58
  }
52
59
 
53
60
  $effect(() => {
54
- const mode = editMode; // read first so Svelte always tracks it as a dependency
55
- if (!grid || !initialized) return;
61
+ const mode = editMode; // force-read so always tracked
62
+ const ready = initialized; // force-read so always tracked
63
+ if (!grid || !ready) return;
56
64
  if (mode) {
57
65
  grid.column(12);
58
66
  grid.enable();
@@ -66,30 +74,28 @@
66
74
  if (!gridEl) return;
67
75
 
68
76
  grid = GridStack.init(
69
- {
70
- column,
71
- cellHeight,
72
- margin: "0.5rem",
73
- float: false,
74
- animate: true,
75
- },
77
+ { column, cellHeight, margin: "0.5rem", float: false, animate: true },
76
78
  gridEl,
77
79
  );
78
80
 
79
81
  lastWidth = gridEl.offsetWidth;
82
+ containerWidth = lastWidth;
80
83
  grid.column(getResponsiveColumns(lastWidth));
81
84
  grid.disable();
82
85
 
83
86
  setTimeout(() => { initialized = true; }, 0);
84
87
 
85
- grid.on("change dragstop resizestop", () => {
88
+ grid.on("dragstop resizestop", () => {
86
89
  grid?.compact();
87
- currentLayout = collectLayout();
90
+ const layout = collectLayout();
91
+ if (saveTimeout) clearTimeout(saveTimeout);
92
+ saveTimeout = setTimeout(() => onLayoutChange?.(layout), 800);
88
93
  });
89
94
 
90
95
  resizeObserver = new ResizeObserver((entries) => {
91
96
  window.dispatchEvent(new Event("resize"));
92
97
  lastWidth = entries[0]?.contentRect.width ?? gridEl!.offsetWidth;
98
+ containerWidth = lastWidth;
93
99
  if (!editMode) {
94
100
  grid?.column(getResponsiveColumns(lastWidth));
95
101
  }
@@ -98,6 +104,7 @@
98
104
  });
99
105
 
100
106
  onDestroy(() => {
107
+ if (saveTimeout) clearTimeout(saveTimeout);
101
108
  resizeObserver?.disconnect();
102
109
  resizeObserver = null;
103
110
  grid?.destroy(false);
@@ -7,16 +7,14 @@
7
7
  const {
8
8
  SidebarTrigger,
9
9
  CreateDetailViewButton,
10
- Button,
11
10
  Icons,
12
11
  } = utils.components;
13
12
 
14
13
  let report: any = $state(null);
15
14
  let charts: any[] = $state([]);
16
15
  let loading = $state(true);
17
- let editMode = $state(false);
18
- let containerWidth = $state(0);
19
- let currentLayout = $state<{ id: string; order: number; w: number; h: number }[]>([]);
16
+ let saving = $state(false);
17
+ let editable = $state(false);
20
18
 
21
19
  async function loadCharts({ showLoading = true } = {}) {
22
20
  if (showLoading) loading = true;
@@ -33,7 +31,8 @@
33
31
  if (showLoading) loading = false;
34
32
  }
35
33
 
36
- async function saveChartsLayout(changes: { id: string; order: number; w: number; h: number }[]) {
34
+ async function handleLayoutChange(changes: { id: string; order: number; w: number; h: number }[]) {
35
+ saving = true;
37
36
  for (const change of changes) {
38
37
  await utils.lobb.updateOne("reports_charts", change.id, {
39
38
  sort_order: change.order,
@@ -41,6 +40,7 @@
41
40
  row_span: change.h,
42
41
  });
43
42
  }
43
+ saving = false;
44
44
  }
45
45
 
46
46
  function getChartGridSize(chart: any): Record<string, unknown> {
@@ -61,7 +61,7 @@
61
61
  onMount(() => loadCharts());
62
62
  </script>
63
63
 
64
- <div class="report-layout" bind:clientWidth={containerWidth}>
64
+ <div class="report-layout">
65
65
  {#if loading}
66
66
  <div class="flex h-full w-full flex-col items-center justify-center gap-4" style="grid-row: 1 / -1">
67
67
  <Icons.LoaderCircle class="animate-spin opacity-50" size="50" />
@@ -79,51 +79,33 @@
79
79
  <div class="text-xs text-muted-foreground">{report.description}</div>
80
80
  </div>
81
81
  </div>
82
- <div class="flex gap-2 self-end">
83
- {#if editMode}
84
- <Button
85
- variant="ghost"
86
- class="h-7 px-3 text-xs font-normal text-muted-foreground"
87
- onclick={() => {
88
- editMode = false;
89
- loadCharts({ showLoading: false });
90
- }}
91
- >
92
- Cancel
93
- </Button>
94
- <Button
95
- variant="default"
96
- class="h-7 px-3 text-xs font-normal"
97
- Icon={Icons.Check}
98
- onclick={async () => {
99
- await saveChartsLayout(currentLayout);
100
- editMode = false;
101
- }}
102
- >
103
- Save layout
104
- </Button>
82
+ <div class="flex items-center gap-2 self-end">
83
+ {#if saving}
84
+ <div class="flex items-center gap-1.5 text-xs text-muted-foreground">
85
+ <Icons.LoaderCircle class="animate-spin" size={12} />
86
+ Saving...
87
+ </div>
88
+ {:else if editable}
89
+ <div class="flex items-center gap-1.5 text-xs text-muted-foreground">
90
+ <div class="h-1.5 w-1.5 rounded-full bg-green-500"></div>
91
+ Layout editable
92
+ </div>
105
93
  {:else}
106
- {#if containerWidth >= 1200}
107
- <Button
108
- variant="outline"
109
- class="h-7 px-3 text-xs font-normal"
110
- Icon={Icons.LayoutDashboard}
111
- onclick={() => (editMode = true)}
112
- >
113
- Edit layout
114
- </Button>
115
- {/if}
116
- <CreateDetailViewButton
117
- collectionName="reports_charts"
118
- values={{ report_id: { id: reportId, name: report.name } }}
119
- variant="default"
120
- class="h-7 px-3 text-xs font-normal"
121
- Icon={Icons.Plus}
122
- onSuccessfullSave={async () => await loadCharts()}
123
- >
124
- Create chart
125
- </CreateDetailViewButton>
94
+ <div class="flex items-center gap-1.5 text-xs text-muted-foreground">
95
+ <div class="h-1.5 w-1.5 rounded-full bg-muted-foreground/40"></div>
96
+ Expand window to rearrange
97
+ </div>
126
98
  {/if}
99
+ <CreateDetailViewButton
100
+ collectionName="reports_charts"
101
+ values={{ report_id: { id: reportId, name: report.name } }}
102
+ variant="default"
103
+ class="h-7 px-3 text-xs font-normal"
104
+ Icon={Icons.Plus}
105
+ onSuccessfullSave={async () => await loadCharts()}
106
+ >
107
+ Create chart
108
+ </CreateDetailViewButton>
127
109
  </div>
128
110
  </div>
129
111
 
@@ -138,7 +120,7 @@
138
120
  </div>
139
121
  {:else}
140
122
  {#key charts}
141
- <GridStackComponent bind:editMode bind:currentLayout>
123
+ <GridStackComponent bind:editable onLayoutChange={handleLayoutChange}>
142
124
  {#each [...charts].sort((a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0)) as chart (chart.id)}
143
125
  <div
144
126
  class="grid-stack-item"
@@ -105,7 +105,7 @@
105
105
  </div>
106
106
  {/if}
107
107
  {/snippet}
108
- <div class="flex h-full w-full flex-col bg-muted">
108
+ <div class="relative flex h-full w-full flex-col bg-muted">
109
109
  {#if reportId}
110
110
  {#key reportId}
111
111
  <Report {utils} {reportId} {...props} />
@@ -8,24 +8,31 @@
8
8
  interface Props {
9
9
  column?: number;
10
10
  cellHeight?: string;
11
- editMode?: boolean;
12
- currentLayout?: LayoutItem[];
11
+ editable?: boolean;
12
+ onLayoutChange?: (layout: LayoutItem[]) => void;
13
13
  children: Snippet;
14
14
  }
15
15
 
16
16
  let {
17
17
  column = 12,
18
18
  cellHeight = "150px",
19
- editMode = $bindable(false),
20
- currentLayout = $bindable([]),
19
+ editable = $bindable(false),
20
+ onLayoutChange,
21
21
  children,
22
22
  }: Props = $props();
23
23
 
24
24
  let gridEl: HTMLElement | undefined = $state();
25
25
  let grid: GridStack | null = null;
26
- let initialized = false;
26
+ let initialized = $state(false);
27
27
  let resizeObserver: ResizeObserver | null = null;
28
28
  let lastWidth = 0;
29
+ let containerWidth = $state(0);
30
+ let saveTimeout: ReturnType<typeof setTimeout> | null = null;
31
+
32
+ let editMode = $derived(containerWidth >= 1200);
33
+
34
+ // Keep editable in sync so parent can read it
35
+ $effect(() => { editable = editMode; });
29
36
 
30
37
  function getResponsiveColumns(width: number): number {
31
38
  if (width >= 1200) return 12;
@@ -51,8 +58,9 @@
51
58
  }
52
59
 
53
60
  $effect(() => {
54
- const mode = editMode; // read first so Svelte always tracks it as a dependency
55
- if (!grid || !initialized) return;
61
+ const mode = editMode; // force-read so always tracked
62
+ const ready = initialized; // force-read so always tracked
63
+ if (!grid || !ready) return;
56
64
  if (mode) {
57
65
  grid.column(12);
58
66
  grid.enable();
@@ -66,30 +74,28 @@
66
74
  if (!gridEl) return;
67
75
 
68
76
  grid = GridStack.init(
69
- {
70
- column,
71
- cellHeight,
72
- margin: "0.5rem",
73
- float: false,
74
- animate: true,
75
- },
77
+ { column, cellHeight, margin: "0.5rem", float: false, animate: true },
76
78
  gridEl,
77
79
  );
78
80
 
79
81
  lastWidth = gridEl.offsetWidth;
82
+ containerWidth = lastWidth;
80
83
  grid.column(getResponsiveColumns(lastWidth));
81
84
  grid.disable();
82
85
 
83
86
  setTimeout(() => { initialized = true; }, 0);
84
87
 
85
- grid.on("change dragstop resizestop", () => {
88
+ grid.on("dragstop resizestop", () => {
86
89
  grid?.compact();
87
- currentLayout = collectLayout();
90
+ const layout = collectLayout();
91
+ if (saveTimeout) clearTimeout(saveTimeout);
92
+ saveTimeout = setTimeout(() => onLayoutChange?.(layout), 800);
88
93
  });
89
94
 
90
95
  resizeObserver = new ResizeObserver((entries) => {
91
96
  window.dispatchEvent(new Event("resize"));
92
97
  lastWidth = entries[0]?.contentRect.width ?? gridEl!.offsetWidth;
98
+ containerWidth = lastWidth;
93
99
  if (!editMode) {
94
100
  grid?.column(getResponsiveColumns(lastWidth));
95
101
  }
@@ -98,6 +104,7 @@
98
104
  });
99
105
 
100
106
  onDestroy(() => {
107
+ if (saveTimeout) clearTimeout(saveTimeout);
101
108
  resizeObserver?.disconnect();
102
109
  resizeObserver = null;
103
110
  grid?.destroy(false);
@@ -7,16 +7,14 @@
7
7
  const {
8
8
  SidebarTrigger,
9
9
  CreateDetailViewButton,
10
- Button,
11
10
  Icons,
12
11
  } = utils.components;
13
12
 
14
13
  let report: any = $state(null);
15
14
  let charts: any[] = $state([]);
16
15
  let loading = $state(true);
17
- let editMode = $state(false);
18
- let containerWidth = $state(0);
19
- let currentLayout = $state<{ id: string; order: number; w: number; h: number }[]>([]);
16
+ let saving = $state(false);
17
+ let editable = $state(false);
20
18
 
21
19
  async function loadCharts({ showLoading = true } = {}) {
22
20
  if (showLoading) loading = true;
@@ -33,7 +31,8 @@
33
31
  if (showLoading) loading = false;
34
32
  }
35
33
 
36
- async function saveChartsLayout(changes: { id: string; order: number; w: number; h: number }[]) {
34
+ async function handleLayoutChange(changes: { id: string; order: number; w: number; h: number }[]) {
35
+ saving = true;
37
36
  for (const change of changes) {
38
37
  await utils.lobb.updateOne("reports_charts", change.id, {
39
38
  sort_order: change.order,
@@ -41,6 +40,7 @@
41
40
  row_span: change.h,
42
41
  });
43
42
  }
43
+ saving = false;
44
44
  }
45
45
 
46
46
  function getChartGridSize(chart: any): Record<string, unknown> {
@@ -61,7 +61,7 @@
61
61
  onMount(() => loadCharts());
62
62
  </script>
63
63
 
64
- <div class="report-layout" bind:clientWidth={containerWidth}>
64
+ <div class="report-layout">
65
65
  {#if loading}
66
66
  <div class="flex h-full w-full flex-col items-center justify-center gap-4" style="grid-row: 1 / -1">
67
67
  <Icons.LoaderCircle class="animate-spin opacity-50" size="50" />
@@ -79,51 +79,33 @@
79
79
  <div class="text-xs text-muted-foreground">{report.description}</div>
80
80
  </div>
81
81
  </div>
82
- <div class="flex gap-2 self-end">
83
- {#if editMode}
84
- <Button
85
- variant="ghost"
86
- class="h-7 px-3 text-xs font-normal text-muted-foreground"
87
- onclick={() => {
88
- editMode = false;
89
- loadCharts({ showLoading: false });
90
- }}
91
- >
92
- Cancel
93
- </Button>
94
- <Button
95
- variant="default"
96
- class="h-7 px-3 text-xs font-normal"
97
- Icon={Icons.Check}
98
- onclick={async () => {
99
- await saveChartsLayout(currentLayout);
100
- editMode = false;
101
- }}
102
- >
103
- Save layout
104
- </Button>
82
+ <div class="flex items-center gap-2 self-end">
83
+ {#if saving}
84
+ <div class="flex items-center gap-1.5 text-xs text-muted-foreground">
85
+ <Icons.LoaderCircle class="animate-spin" size={12} />
86
+ Saving...
87
+ </div>
88
+ {:else if editable}
89
+ <div class="flex items-center gap-1.5 text-xs text-muted-foreground">
90
+ <div class="h-1.5 w-1.5 rounded-full bg-green-500"></div>
91
+ Layout editable
92
+ </div>
105
93
  {:else}
106
- {#if containerWidth >= 1200}
107
- <Button
108
- variant="outline"
109
- class="h-7 px-3 text-xs font-normal"
110
- Icon={Icons.LayoutDashboard}
111
- onclick={() => (editMode = true)}
112
- >
113
- Edit layout
114
- </Button>
115
- {/if}
116
- <CreateDetailViewButton
117
- collectionName="reports_charts"
118
- values={{ report_id: { id: reportId, name: report.name } }}
119
- variant="default"
120
- class="h-7 px-3 text-xs font-normal"
121
- Icon={Icons.Plus}
122
- onSuccessfullSave={async () => await loadCharts()}
123
- >
124
- Create chart
125
- </CreateDetailViewButton>
94
+ <div class="flex items-center gap-1.5 text-xs text-muted-foreground">
95
+ <div class="h-1.5 w-1.5 rounded-full bg-muted-foreground/40"></div>
96
+ Expand window to rearrange
97
+ </div>
126
98
  {/if}
99
+ <CreateDetailViewButton
100
+ collectionName="reports_charts"
101
+ values={{ report_id: { id: reportId, name: report.name } }}
102
+ variant="default"
103
+ class="h-7 px-3 text-xs font-normal"
104
+ Icon={Icons.Plus}
105
+ onSuccessfullSave={async () => await loadCharts()}
106
+ >
107
+ Create chart
108
+ </CreateDetailViewButton>
127
109
  </div>
128
110
  </div>
129
111
 
@@ -138,7 +120,7 @@
138
120
  </div>
139
121
  {:else}
140
122
  {#key charts}
141
- <GridStackComponent bind:editMode bind:currentLayout>
123
+ <GridStackComponent bind:editable onLayoutChange={handleLayoutChange}>
142
124
  {#each [...charts].sort((a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0)) as chart (chart.id)}
143
125
  <div
144
126
  class="grid-stack-item"
@@ -105,7 +105,7 @@
105
105
  </div>
106
106
  {/if}
107
107
  {/snippet}
108
- <div class="flex h-full w-full flex-col bg-muted">
108
+ <div class="relative flex h-full w-full flex-col bg-muted">
109
109
  {#if reportId}
110
110
  {#key reportId}
111
111
  <Report {utils} {reportId} {...props} />
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobb-js/lobb-ext-reports",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -34,7 +34,7 @@
34
34
  "package": "svelte-package --input extensions/reports/studio"
35
35
  },
36
36
  "dependencies": {
37
- "@lobb-js/core": "^0.25.0",
37
+ "@lobb-js/core": "^0.26.0",
38
38
  "chart.js": "^4.4.8",
39
39
  "gridstack": "^12.6.0",
40
40
  "hono": "^4.7.0",
@@ -46,7 +46,7 @@
46
46
  "devDependencies": {
47
47
  "@faker-js/faker": "^9.6.0",
48
48
  "@playwright/test": "^1.58.2",
49
- "@lobb-js/studio": "^0.21.0",
49
+ "@lobb-js/studio": "^0.24.0",
50
50
  "@lucide/svelte": "^0.563.1",
51
51
  "@sveltejs/adapter-node": "^5.5.4",
52
52
  "@sveltejs/kit": "^2.55.0",