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

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.
@@ -3,17 +3,21 @@
3
3
  import { GridStack } from "gridstack";
4
4
  import "gridstack/dist/gridstack.min.css";
5
5
 
6
+ interface LayoutItem { id: string; order: number; w: number; h: number; }
7
+
6
8
  interface Props {
7
9
  column?: number;
8
10
  cellHeight?: string;
9
- onLayoutChange?: (changes: { id: string; order: number; w: number; h: number }[]) => void;
11
+ editMode?: boolean;
12
+ currentLayout?: LayoutItem[];
10
13
  children: Snippet;
11
14
  }
12
15
 
13
16
  let {
14
17
  column = 12,
15
18
  cellHeight = "150px",
16
- onLayoutChange,
19
+ editMode = $bindable(false),
20
+ currentLayout = $bindable([]),
17
21
  children,
18
22
  }: Props = $props();
19
23
 
@@ -21,6 +25,42 @@
21
25
  let grid: GridStack | null = null;
22
26
  let initialized = false;
23
27
  let resizeObserver: ResizeObserver | null = null;
28
+ let lastWidth = 0;
29
+
30
+ function getResponsiveColumns(width: number): number {
31
+ if (width >= 1200) return 12;
32
+ if (width >= 768) return 6;
33
+ if (width >= 480) return 2;
34
+ return 1;
35
+ }
36
+
37
+ function collectLayout(): LayoutItem[] {
38
+ if (!grid) return [];
39
+ return (grid.engine.nodes as any[])
40
+ .filter((n) => {
41
+ const id = n.el?.getAttribute("data-gs-id");
42
+ return id != null && id !== "null";
43
+ })
44
+ .sort((a, b) => (a.y !== b.y ? a.y - b.y : a.x - b.x))
45
+ .map((node, index) => ({
46
+ id: node.el!.getAttribute("data-gs-id") as string,
47
+ order: index,
48
+ w: node.w,
49
+ h: node.h,
50
+ }));
51
+ }
52
+
53
+ $effect(() => {
54
+ const mode = editMode; // read first so Svelte always tracks it as a dependency
55
+ if (!grid || !initialized) return;
56
+ if (mode) {
57
+ grid.column(12);
58
+ grid.enable();
59
+ } else {
60
+ grid.column(getResponsiveColumns(lastWidth));
61
+ grid.disable();
62
+ }
63
+ });
24
64
 
25
65
  onMount(() => {
26
66
  if (!gridEl) return;
@@ -32,49 +72,26 @@
32
72
  margin: "0.5rem",
33
73
  float: false,
34
74
  animate: true,
35
- columnOpts: {
36
- layout: "moveScale",
37
- breakpointForWindow: false,
38
- breakpoints: [
39
- { w: 480, c: 1 },
40
- ],
41
- },
42
75
  },
43
76
  gridEl,
44
77
  );
45
78
 
79
+ lastWidth = gridEl.offsetWidth;
80
+ grid.column(getResponsiveColumns(lastWidth));
81
+ grid.disable();
82
+
46
83
  setTimeout(() => { initialized = true; }, 0);
47
84
 
48
- grid.on("dragstop resizestop", () => {
85
+ grid.on("change dragstop resizestop", () => {
49
86
  grid?.compact();
50
- });
51
-
52
- grid.on("change", () => {
53
- if (!onLayoutChange || !initialized) return;
54
- const currentCols = grid!.getColumn();
55
- if (currentCols === 1) return;
56
- const sorted = (grid!.engine.nodes as any[])
57
- .filter((n) => {
58
- const id = n.el?.getAttribute("data-gs-id");
59
- return id != null && id !== "null";
60
- })
61
- .sort((a, b) => (a.y !== b.y ? a.y - b.y : a.x - b.x))
62
- .map((node, index) => ({
63
- id: node.el!.getAttribute("data-gs-id") as string,
64
- order: index,
65
- w: Math.round(node.w * (column / currentCols)),
66
- h: node.h,
67
- }));
68
- if (sorted.length > 0) onLayoutChange(sorted);
87
+ currentLayout = collectLayout();
69
88
  });
70
89
 
71
90
  resizeObserver = new ResizeObserver((entries) => {
72
91
  window.dispatchEvent(new Event("resize"));
73
- const width = entries[0]?.contentRect.width ?? gridEl!.offsetWidth;
74
- if (width <= 480) {
75
- grid?.disable();
76
- } else {
77
- grid?.enable();
92
+ lastWidth = entries[0]?.contentRect.width ?? gridEl!.offsetWidth;
93
+ if (!editMode) {
94
+ grid?.column(getResponsiveColumns(lastWidth));
78
95
  }
79
96
  });
80
97
  resizeObserver.observe(gridEl);
@@ -4,16 +4,25 @@
4
4
  import ChartJs from "./charts/chartJs.svelte";
5
5
  import Metric from "./charts/metric.svelte";
6
6
 
7
+ interface ChartClickEvent {
8
+ type: string;
9
+ label: string;
10
+ value: any;
11
+ index: number;
12
+ }
13
+
7
14
  interface Props extends ExtensionProps {
8
15
  chartRecord: any;
9
16
  onChartDeleted?: () => Promise<void>;
10
17
  onChartEdited?: () => Promise<void>;
18
+ onChartClick?: (event: ChartClickEvent) => void;
11
19
  }
12
20
 
13
21
  const {
14
22
  chartRecord,
15
23
  onChartDeleted,
16
24
  onChartEdited,
25
+ onChartClick,
17
26
  ...props
18
27
  }: Props = $props();
19
28
 
@@ -119,13 +128,13 @@
119
128
  {#if data.type === "table"}
120
129
  <Table input={data.input} {...props} />
121
130
  {:else if data.type === "chartjs"}
122
- <ChartJs input={data.input} {...props} />
131
+ <ChartJs input={data.input} onClick={onChartClick} {...props} />
123
132
  {:else if data.type === "metric"}
124
133
  <Metric input={data.input} />
125
134
  {:else}
126
135
  {@const CustomChart = findCustomChartComponent(data.type)}
127
136
  {#if CustomChart}
128
- <CustomChart input={data.input} {...props} />
137
+ <CustomChart input={data.input} onClick={onChartClick} {...props} />
129
138
  {:else}
130
139
  <div class="text-muted-foreground">
131
140
  The ({data.type}) chart type is unsupported
@@ -3,13 +3,13 @@
3
3
 
4
4
  import { onMount } from 'svelte';
5
5
  import { Chart } from 'chart.js/auto';
6
- import { merge } from 'lodash-es';
7
6
 
8
7
  interface Props extends ExtensionProps {
9
8
  input: any;
9
+ onClick?: (event: any) => void;
10
10
  }
11
11
 
12
- const { input }: Props = $props();
12
+ const { input, onClick }: Props = $props();
13
13
 
14
14
  let canvas: HTMLCanvasElement;
15
15
  let chart: Chart;
@@ -18,18 +18,18 @@
18
18
  if (canvas) {
19
19
  const ctx = canvas.getContext('2d');
20
20
  if (ctx) {
21
- chart = new Chart(
22
- ctx,
23
- merge(
24
- {
25
- options: {
26
- responsive: true,
27
- maintainAspectRatio: false
28
- }
29
- },
30
- $state.snapshot(input)
31
- )
32
- );
21
+ const snapshot = $state.snapshot(input);
22
+ chart = new Chart(ctx, {
23
+ ...snapshot,
24
+ options: {
25
+ responsive: true,
26
+ maintainAspectRatio: false,
27
+ ...snapshot.options,
28
+ ...(onClick ? {
29
+ onClick: onClick
30
+ } : {}),
31
+ },
32
+ });
33
33
  }
34
34
  }
35
35
 
@@ -2,21 +2,24 @@
2
2
  import { onMount } from "svelte";
3
3
  import GridStackComponent from "../../../gridStack.svelte";
4
4
  import Chart from "./chart.svelte";
5
- import ExportButton from "./exportButton.svelte";
6
5
 
7
6
  const { reportId, utils, ...props }: { reportId: string; utils: any; [key: string]: any } = $props();
8
7
  const {
9
8
  SidebarTrigger,
10
9
  CreateDetailViewButton,
10
+ Button,
11
11
  Icons,
12
12
  } = utils.components;
13
13
 
14
14
  let report: any = $state(null);
15
15
  let charts: any[] = $state([]);
16
16
  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 }[]>([]);
17
20
 
18
- async function loadCharts() {
19
- loading = true;
21
+ async function loadCharts({ showLoading = true } = {}) {
22
+ if (showLoading) loading = true;
20
23
  const reportsRes = await utils.lobb.findAll("reports_dashboards", {
21
24
  sort: "id",
22
25
  filter: { id: reportId },
@@ -27,10 +30,10 @@
27
30
  filter: { report_id: report.id },
28
31
  });
29
32
  charts = (await chartsRes.json()).data;
30
- loading = false;
33
+ if (showLoading) loading = false;
31
34
  }
32
35
 
33
- async function handleLayoutChange(changes: { id: string; order: number; w: number; h: number }[]) {
36
+ async function saveChartsLayout(changes: { id: string; order: number; w: number; h: number }[]) {
34
37
  for (const change of changes) {
35
38
  await utils.lobb.updateOne("reports_charts", change.id, {
36
39
  sort_order: change.order,
@@ -40,14 +43,25 @@
40
43
  }
41
44
  }
42
45
 
43
- function gridItemAttrs(chart: any): Record<string, unknown> {
46
+ function getChartGridSize(chart: any): Record<string, unknown> {
44
47
  return { "gs-w": chart.col_span ?? 6, "gs-h": chart.row_span ?? 2, "gs-auto-position": "true" };
45
48
  }
46
49
 
50
+ function handleChartClick(_event: any, elements: any[], chart: any) {
51
+ if (!elements.length) return;
52
+ const { index, datasetIndex } = elements[0];
53
+ const dataset = chart.data.datasets[datasetIndex];
54
+ utils.emitEvent("reports.chart.click", {
55
+ label: chart.data.labels?.[index],
56
+ value: dataset.data[index],
57
+ ids: dataset.ids?.[index],
58
+ });
59
+ }
60
+
47
61
  onMount(() => loadCharts());
48
62
  </script>
49
63
 
50
- <div class="report-layout">
64
+ <div class="report-layout" bind:clientWidth={containerWidth}>
51
65
  {#if loading}
52
66
  <div class="flex h-full w-full flex-col items-center justify-center gap-4" style="grid-row: 1 / -1">
53
67
  <Icons.LoaderCircle class="animate-spin opacity-50" size="50" />
@@ -66,17 +80,50 @@
66
80
  </div>
67
81
  </div>
68
82
  <div class="flex gap-2 self-end">
69
- <ExportButton {report} {utils} />
70
- <CreateDetailViewButton
71
- collectionName="reports_charts"
72
- values={{ report_id: { id: props.reportId, name: report.name } }}
73
- variant="default"
74
- class="h-7 px-3 text-xs font-normal"
75
- Icon={Icons.Plus}
76
- onSuccessfullSave={async () => await loadCharts()}
77
- >
78
- Create chart
79
- </CreateDetailViewButton>
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>
105
+ {: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>
126
+ {/if}
80
127
  </div>
81
128
  </div>
82
129
 
@@ -91,18 +138,19 @@
91
138
  </div>
92
139
  {:else}
93
140
  {#key charts}
94
- <GridStackComponent onLayoutChange={handleLayoutChange}>
141
+ <GridStackComponent bind:editMode bind:currentLayout>
95
142
  {#each [...charts].sort((a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0)) as chart (chart.id)}
96
143
  <div
97
144
  class="grid-stack-item"
98
145
  data-gs-id={chart.id}
99
- {...gridItemAttrs(chart)}
146
+ {...getChartGridSize(chart)}
100
147
  >
101
148
  <div class="grid-stack-item-content">
102
149
  <Chart
103
150
  chartRecord={chart}
104
151
  onChartDeleted={async () => await loadCharts()}
105
152
  onChartEdited={async () => await loadCharts()}
153
+ onChartClick={handleChartClick}
106
154
  {utils}
107
155
  {...props}
108
156
  />
@@ -3,17 +3,21 @@
3
3
  import { GridStack } from "gridstack";
4
4
  import "gridstack/dist/gridstack.min.css";
5
5
 
6
+ interface LayoutItem { id: string; order: number; w: number; h: number; }
7
+
6
8
  interface Props {
7
9
  column?: number;
8
10
  cellHeight?: string;
9
- onLayoutChange?: (changes: { id: string; order: number; w: number; h: number }[]) => void;
11
+ editMode?: boolean;
12
+ currentLayout?: LayoutItem[];
10
13
  children: Snippet;
11
14
  }
12
15
 
13
16
  let {
14
17
  column = 12,
15
18
  cellHeight = "150px",
16
- onLayoutChange,
19
+ editMode = $bindable(false),
20
+ currentLayout = $bindable([]),
17
21
  children,
18
22
  }: Props = $props();
19
23
 
@@ -21,6 +25,42 @@
21
25
  let grid: GridStack | null = null;
22
26
  let initialized = false;
23
27
  let resizeObserver: ResizeObserver | null = null;
28
+ let lastWidth = 0;
29
+
30
+ function getResponsiveColumns(width: number): number {
31
+ if (width >= 1200) return 12;
32
+ if (width >= 768) return 6;
33
+ if (width >= 480) return 2;
34
+ return 1;
35
+ }
36
+
37
+ function collectLayout(): LayoutItem[] {
38
+ if (!grid) return [];
39
+ return (grid.engine.nodes as any[])
40
+ .filter((n) => {
41
+ const id = n.el?.getAttribute("data-gs-id");
42
+ return id != null && id !== "null";
43
+ })
44
+ .sort((a, b) => (a.y !== b.y ? a.y - b.y : a.x - b.x))
45
+ .map((node, index) => ({
46
+ id: node.el!.getAttribute("data-gs-id") as string,
47
+ order: index,
48
+ w: node.w,
49
+ h: node.h,
50
+ }));
51
+ }
52
+
53
+ $effect(() => {
54
+ const mode = editMode; // read first so Svelte always tracks it as a dependency
55
+ if (!grid || !initialized) return;
56
+ if (mode) {
57
+ grid.column(12);
58
+ grid.enable();
59
+ } else {
60
+ grid.column(getResponsiveColumns(lastWidth));
61
+ grid.disable();
62
+ }
63
+ });
24
64
 
25
65
  onMount(() => {
26
66
  if (!gridEl) return;
@@ -32,49 +72,26 @@
32
72
  margin: "0.5rem",
33
73
  float: false,
34
74
  animate: true,
35
- columnOpts: {
36
- layout: "moveScale",
37
- breakpointForWindow: false,
38
- breakpoints: [
39
- { w: 480, c: 1 },
40
- ],
41
- },
42
75
  },
43
76
  gridEl,
44
77
  );
45
78
 
79
+ lastWidth = gridEl.offsetWidth;
80
+ grid.column(getResponsiveColumns(lastWidth));
81
+ grid.disable();
82
+
46
83
  setTimeout(() => { initialized = true; }, 0);
47
84
 
48
- grid.on("dragstop resizestop", () => {
85
+ grid.on("change dragstop resizestop", () => {
49
86
  grid?.compact();
50
- });
51
-
52
- grid.on("change", () => {
53
- if (!onLayoutChange || !initialized) return;
54
- const currentCols = grid!.getColumn();
55
- if (currentCols === 1) return;
56
- const sorted = (grid!.engine.nodes as any[])
57
- .filter((n) => {
58
- const id = n.el?.getAttribute("data-gs-id");
59
- return id != null && id !== "null";
60
- })
61
- .sort((a, b) => (a.y !== b.y ? a.y - b.y : a.x - b.x))
62
- .map((node, index) => ({
63
- id: node.el!.getAttribute("data-gs-id") as string,
64
- order: index,
65
- w: Math.round(node.w * (column / currentCols)),
66
- h: node.h,
67
- }));
68
- if (sorted.length > 0) onLayoutChange(sorted);
87
+ currentLayout = collectLayout();
69
88
  });
70
89
 
71
90
  resizeObserver = new ResizeObserver((entries) => {
72
91
  window.dispatchEvent(new Event("resize"));
73
- const width = entries[0]?.contentRect.width ?? gridEl!.offsetWidth;
74
- if (width <= 480) {
75
- grid?.disable();
76
- } else {
77
- grid?.enable();
92
+ lastWidth = entries[0]?.contentRect.width ?? gridEl!.offsetWidth;
93
+ if (!editMode) {
94
+ grid?.column(getResponsiveColumns(lastWidth));
78
95
  }
79
96
  });
80
97
  resizeObserver.observe(gridEl);
@@ -4,16 +4,25 @@
4
4
  import ChartJs from "./charts/chartJs.svelte";
5
5
  import Metric from "./charts/metric.svelte";
6
6
 
7
+ interface ChartClickEvent {
8
+ type: string;
9
+ label: string;
10
+ value: any;
11
+ index: number;
12
+ }
13
+
7
14
  interface Props extends ExtensionProps {
8
15
  chartRecord: any;
9
16
  onChartDeleted?: () => Promise<void>;
10
17
  onChartEdited?: () => Promise<void>;
18
+ onChartClick?: (event: ChartClickEvent) => void;
11
19
  }
12
20
 
13
21
  const {
14
22
  chartRecord,
15
23
  onChartDeleted,
16
24
  onChartEdited,
25
+ onChartClick,
17
26
  ...props
18
27
  }: Props = $props();
19
28
 
@@ -119,13 +128,13 @@
119
128
  {#if data.type === "table"}
120
129
  <Table input={data.input} {...props} />
121
130
  {:else if data.type === "chartjs"}
122
- <ChartJs input={data.input} {...props} />
131
+ <ChartJs input={data.input} onClick={onChartClick} {...props} />
123
132
  {:else if data.type === "metric"}
124
133
  <Metric input={data.input} />
125
134
  {:else}
126
135
  {@const CustomChart = findCustomChartComponent(data.type)}
127
136
  {#if CustomChart}
128
- <CustomChart input={data.input} {...props} />
137
+ <CustomChart input={data.input} onClick={onChartClick} {...props} />
129
138
  {:else}
130
139
  <div class="text-muted-foreground">
131
140
  The ({data.type}) chart type is unsupported
@@ -3,13 +3,13 @@
3
3
 
4
4
  import { onMount } from 'svelte';
5
5
  import { Chart } from 'chart.js/auto';
6
- import { merge } from 'lodash-es';
7
6
 
8
7
  interface Props extends ExtensionProps {
9
8
  input: any;
9
+ onClick?: (event: any) => void;
10
10
  }
11
11
 
12
- const { input }: Props = $props();
12
+ const { input, onClick }: Props = $props();
13
13
 
14
14
  let canvas: HTMLCanvasElement;
15
15
  let chart: Chart;
@@ -18,18 +18,18 @@
18
18
  if (canvas) {
19
19
  const ctx = canvas.getContext('2d');
20
20
  if (ctx) {
21
- chart = new Chart(
22
- ctx,
23
- merge(
24
- {
25
- options: {
26
- responsive: true,
27
- maintainAspectRatio: false
28
- }
29
- },
30
- $state.snapshot(input)
31
- )
32
- );
21
+ const snapshot = $state.snapshot(input);
22
+ chart = new Chart(ctx, {
23
+ ...snapshot,
24
+ options: {
25
+ responsive: true,
26
+ maintainAspectRatio: false,
27
+ ...snapshot.options,
28
+ ...(onClick ? {
29
+ onClick: onClick
30
+ } : {}),
31
+ },
32
+ });
33
33
  }
34
34
  }
35
35
 
@@ -2,21 +2,24 @@
2
2
  import { onMount } from "svelte";
3
3
  import GridStackComponent from "../../../gridStack.svelte";
4
4
  import Chart from "./chart.svelte";
5
- import ExportButton from "./exportButton.svelte";
6
5
 
7
6
  const { reportId, utils, ...props }: { reportId: string; utils: any; [key: string]: any } = $props();
8
7
  const {
9
8
  SidebarTrigger,
10
9
  CreateDetailViewButton,
10
+ Button,
11
11
  Icons,
12
12
  } = utils.components;
13
13
 
14
14
  let report: any = $state(null);
15
15
  let charts: any[] = $state([]);
16
16
  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 }[]>([]);
17
20
 
18
- async function loadCharts() {
19
- loading = true;
21
+ async function loadCharts({ showLoading = true } = {}) {
22
+ if (showLoading) loading = true;
20
23
  const reportsRes = await utils.lobb.findAll("reports_dashboards", {
21
24
  sort: "id",
22
25
  filter: { id: reportId },
@@ -27,10 +30,10 @@
27
30
  filter: { report_id: report.id },
28
31
  });
29
32
  charts = (await chartsRes.json()).data;
30
- loading = false;
33
+ if (showLoading) loading = false;
31
34
  }
32
35
 
33
- async function handleLayoutChange(changes: { id: string; order: number; w: number; h: number }[]) {
36
+ async function saveChartsLayout(changes: { id: string; order: number; w: number; h: number }[]) {
34
37
  for (const change of changes) {
35
38
  await utils.lobb.updateOne("reports_charts", change.id, {
36
39
  sort_order: change.order,
@@ -40,14 +43,25 @@
40
43
  }
41
44
  }
42
45
 
43
- function gridItemAttrs(chart: any): Record<string, unknown> {
46
+ function getChartGridSize(chart: any): Record<string, unknown> {
44
47
  return { "gs-w": chart.col_span ?? 6, "gs-h": chart.row_span ?? 2, "gs-auto-position": "true" };
45
48
  }
46
49
 
50
+ function handleChartClick(_event: any, elements: any[], chart: any) {
51
+ if (!elements.length) return;
52
+ const { index, datasetIndex } = elements[0];
53
+ const dataset = chart.data.datasets[datasetIndex];
54
+ utils.emitEvent("reports.chart.click", {
55
+ label: chart.data.labels?.[index],
56
+ value: dataset.data[index],
57
+ ids: dataset.ids?.[index],
58
+ });
59
+ }
60
+
47
61
  onMount(() => loadCharts());
48
62
  </script>
49
63
 
50
- <div class="report-layout">
64
+ <div class="report-layout" bind:clientWidth={containerWidth}>
51
65
  {#if loading}
52
66
  <div class="flex h-full w-full flex-col items-center justify-center gap-4" style="grid-row: 1 / -1">
53
67
  <Icons.LoaderCircle class="animate-spin opacity-50" size="50" />
@@ -66,17 +80,50 @@
66
80
  </div>
67
81
  </div>
68
82
  <div class="flex gap-2 self-end">
69
- <ExportButton {report} {utils} />
70
- <CreateDetailViewButton
71
- collectionName="reports_charts"
72
- values={{ report_id: { id: props.reportId, name: report.name } }}
73
- variant="default"
74
- class="h-7 px-3 text-xs font-normal"
75
- Icon={Icons.Plus}
76
- onSuccessfullSave={async () => await loadCharts()}
77
- >
78
- Create chart
79
- </CreateDetailViewButton>
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>
105
+ {: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>
126
+ {/if}
80
127
  </div>
81
128
  </div>
82
129
 
@@ -91,18 +138,19 @@
91
138
  </div>
92
139
  {:else}
93
140
  {#key charts}
94
- <GridStackComponent onLayoutChange={handleLayoutChange}>
141
+ <GridStackComponent bind:editMode bind:currentLayout>
95
142
  {#each [...charts].sort((a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0)) as chart (chart.id)}
96
143
  <div
97
144
  class="grid-stack-item"
98
145
  data-gs-id={chart.id}
99
- {...gridItemAttrs(chart)}
146
+ {...getChartGridSize(chart)}
100
147
  >
101
148
  <div class="grid-stack-item-content">
102
149
  <Chart
103
150
  chartRecord={chart}
104
151
  onChartDeleted={async () => await loadCharts()}
105
152
  onChartEdited={async () => await loadCharts()}
153
+ onChartClick={handleChartClick}
106
154
  {utils}
107
155
  {...props}
108
156
  />
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobb-js/lobb-ext-reports",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
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.23.0",
37
+ "@lobb-js/core": "^0.25.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.19.0",
49
+ "@lobb-js/studio": "^0.21.0",
50
50
  "@lucide/svelte": "^0.563.1",
51
51
  "@sveltejs/adapter-node": "^5.5.4",
52
52
  "@sveltejs/kit": "^2.55.0",
@@ -1,57 +0,0 @@
1
- <script lang="ts">
2
- import { toPng } from "html-to-image";
3
- import { jsPDF } from "jspdf";
4
-
5
- interface Props {
6
- report: any;
7
- utils: any;
8
- }
9
-
10
- const { report, utils }: Props = $props();
11
- const { Button, Icons } = utils.components;
12
-
13
- let exporting = $state(false);
14
-
15
- async function handleExport() {
16
- const container = document.querySelector<HTMLElement>(".charts-export-container");
17
- if (!container || !report) return;
18
- exporting = true;
19
-
20
- try {
21
- const backgroundColor = getComputedStyle(document.documentElement).backgroundColor;
22
-
23
- const imgData = await toPng(container, {
24
- pixelRatio: 2,
25
- backgroundColor,
26
- style: {
27
- overflow: "visible",
28
- height: `${container.scrollHeight}px`,
29
- },
30
- });
31
-
32
- const img = new Image();
33
- img.src = imgData;
34
- await new Promise((r) => (img.onload = r));
35
-
36
- const pdf = new jsPDF({ orientation: "landscape", unit: "px" });
37
- const pageWidth = pdf.internal.pageSize.getWidth();
38
- const pageHeight = pdf.internal.pageSize.getHeight();
39
- const ratio = Math.min(pageWidth / img.width, pageHeight / img.height);
40
-
41
- pdf.addImage(imgData, "PNG", 0, 0, img.width * ratio, img.height * ratio);
42
- pdf.save(`${report.name ?? "report"}.pdf`);
43
- } finally {
44
- exporting = false;
45
- }
46
- }
47
- </script>
48
-
49
- <Button
50
- variant="outline"
51
- class="h-7 px-3 text-xs font-normal"
52
- Icon={exporting ? Icons.LoaderCircle : Icons.Upload}
53
- onclick={handleExport}
54
- disabled={exporting}
55
- >
56
- {exporting ? "Exporting..." : "Export"}
57
- </Button>
@@ -1,14 +0,0 @@
1
- import { SvelteComponentTyped } from "svelte";
2
- declare const __propDef: {
3
- props: Record<string, never>;
4
- events: {
5
- [evt: string]: CustomEvent<any>;
6
- };
7
- slots: {};
8
- };
9
- export type ExportButtonProps = typeof __propDef.props;
10
- export type ExportButtonEvents = typeof __propDef.events;
11
- export type ExportButtonSlots = typeof __propDef.slots;
12
- export default class ExportButton extends SvelteComponentTyped<ExportButtonProps, ExportButtonEvents, ExportButtonSlots> {
13
- }
14
- export {};
@@ -1,57 +0,0 @@
1
- <script lang="ts">
2
- import { toPng } from "html-to-image";
3
- import { jsPDF } from "jspdf";
4
-
5
- interface Props {
6
- report: any;
7
- utils: any;
8
- }
9
-
10
- const { report, utils }: Props = $props();
11
- const { Button, Icons } = utils.components;
12
-
13
- let exporting = $state(false);
14
-
15
- async function handleExport() {
16
- const container = document.querySelector<HTMLElement>(".charts-export-container");
17
- if (!container || !report) return;
18
- exporting = true;
19
-
20
- try {
21
- const backgroundColor = getComputedStyle(document.documentElement).backgroundColor;
22
-
23
- const imgData = await toPng(container, {
24
- pixelRatio: 2,
25
- backgroundColor,
26
- style: {
27
- overflow: "visible",
28
- height: `${container.scrollHeight}px`,
29
- },
30
- });
31
-
32
- const img = new Image();
33
- img.src = imgData;
34
- await new Promise((r) => (img.onload = r));
35
-
36
- const pdf = new jsPDF({ orientation: "landscape", unit: "px" });
37
- const pageWidth = pdf.internal.pageSize.getWidth();
38
- const pageHeight = pdf.internal.pageSize.getHeight();
39
- const ratio = Math.min(pageWidth / img.width, pageHeight / img.height);
40
-
41
- pdf.addImage(imgData, "PNG", 0, 0, img.width * ratio, img.height * ratio);
42
- pdf.save(`${report.name ?? "report"}.pdf`);
43
- } finally {
44
- exporting = false;
45
- }
46
- }
47
- </script>
48
-
49
- <Button
50
- variant="outline"
51
- class="h-7 px-3 text-xs font-normal"
52
- Icon={exporting ? Icons.LoaderCircle : Icons.Upload}
53
- onclick={handleExport}
54
- disabled={exporting}
55
- >
56
- {exporting ? "Exporting..." : "Export"}
57
- </Button>