@multiplatform.one/frappe-ui 7.35.0 → 7.36.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.
- package/dist/cjs/FrappeReportBuilder.cjs +9 -3
- package/dist/cjs/FrappeReportBuilder.native.js +12 -4
- package/dist/cjs/FrappeReportBuilder.native.js.map +1 -1
- package/dist/cjs/report/ReportGrid.cjs +123 -23
- package/dist/cjs/report/ReportGrid.native.js +126 -16
- package/dist/cjs/report/ReportGrid.native.js.map +1 -1
- package/dist/cjs/report/ReportViewToolbar.cjs +1 -1
- package/dist/cjs/report/ReportViewToolbar.native.js +1 -1
- package/dist/cjs/report/ReportViewToolbar.native.js.map +1 -1
- package/dist/cjs/report/reportViewModel.cjs +20 -0
- package/dist/cjs/report/reportViewModel.native.js +89 -0
- package/dist/cjs/report/reportViewModel.native.js.map +1 -1
- package/dist/esm/FrappeReportBuilder.mjs +11 -5
- package/dist/esm/FrappeReportBuilder.mjs.map +1 -1
- package/dist/esm/FrappeReportBuilder.native.js +14 -6
- package/dist/esm/FrappeReportBuilder.native.js.map +1 -1
- package/dist/esm/report/ReportGrid.mjs +125 -26
- package/dist/esm/report/ReportGrid.mjs.map +1 -1
- package/dist/esm/report/ReportGrid.native.js +128 -19
- package/dist/esm/report/ReportGrid.native.js.map +1 -1
- package/dist/esm/report/ReportViewToolbar.mjs +1 -1
- package/dist/esm/report/ReportViewToolbar.native.js +1 -1
- package/dist/esm/report/ReportViewToolbar.native.js.map +1 -1
- package/dist/esm/report/reportViewModel.mjs +19 -1
- package/dist/esm/report/reportViewModel.mjs.map +1 -1
- package/dist/esm/report/reportViewModel.native.js +88 -1
- package/dist/esm/report/reportViewModel.native.js.map +1 -1
- package/dist/jsx/FrappeReportBuilder.mjs +11 -5
- package/dist/jsx/FrappeReportBuilder.mjs.map +1 -1
- package/dist/jsx/FrappeReportBuilder.native.js +12 -4
- package/dist/jsx/FrappeReportBuilder.native.js.map +1 -1
- package/dist/jsx/report/ReportGrid.mjs +125 -26
- package/dist/jsx/report/ReportGrid.mjs.map +1 -1
- package/dist/jsx/report/ReportGrid.native.js +126 -16
- package/dist/jsx/report/ReportGrid.native.js.map +1 -1
- package/dist/jsx/report/ReportViewToolbar.mjs +1 -1
- package/dist/jsx/report/ReportViewToolbar.native.js +1 -1
- package/dist/jsx/report/reportViewModel.mjs +19 -1
- package/dist/jsx/report/reportViewModel.mjs.map +1 -1
- package/dist/jsx/report/reportViewModel.native.js +89 -0
- package/dist/jsx/report/reportViewModel.native.js.map +1 -1
- package/package.json +16 -16
- package/src/FrappeReportBuilder.tsx +29 -4
- package/src/report/ReportGrid.phone.spec.tsx +140 -0
- package/src/report/ReportGrid.tsx +218 -71
- package/src/report/ReportViewToolbar.tsx +1 -1
- package/src/report/reportViewModel.ts +38 -0
- package/types/FrappeReportBuilder.d.ts.map +1 -1
- package/types/report/ReportGrid.d.ts +19 -1
- package/types/report/ReportGrid.d.ts.map +1 -1
- package/types/report/reportViewModel.d.ts +10 -0
- package/types/report/reportViewModel.d.ts.map +1 -1
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// The phone report Clay picked (FZ-7684, option B): four columns that fit
|
|
2
|
+
// the screen, the record's image inside the title cell, units in the number
|
|
3
|
+
// cells, and no row-number column or per-column filter row.
|
|
4
|
+
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
5
|
+
import { screen } from "@testing-library/react";
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
7
|
+
import { ReportGrid, reportColumnUnit } from "./ReportGrid";
|
|
8
|
+
import {
|
|
9
|
+
buildGridRows,
|
|
10
|
+
phoneReportFieldnames,
|
|
11
|
+
type ReportColumnDef,
|
|
12
|
+
type ReportRow,
|
|
13
|
+
} from "./reportViewModel";
|
|
14
|
+
|
|
15
|
+
const columns: ReportColumnDef[] = [
|
|
16
|
+
{
|
|
17
|
+
fieldname: "pokemon_name",
|
|
18
|
+
label: "Pokemon Name",
|
|
19
|
+
fieldtype: "Data",
|
|
20
|
+
kind: "text",
|
|
21
|
+
width: 160,
|
|
22
|
+
align: "left",
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
fieldname: "poke_id",
|
|
26
|
+
label: "Poke ID",
|
|
27
|
+
fieldtype: "Int",
|
|
28
|
+
kind: "number",
|
|
29
|
+
width: 110,
|
|
30
|
+
align: "right",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
fieldname: "height",
|
|
34
|
+
label: "Height (dm)",
|
|
35
|
+
fieldtype: "Int",
|
|
36
|
+
kind: "number",
|
|
37
|
+
width: 110,
|
|
38
|
+
align: "right",
|
|
39
|
+
},
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
const rows: ReportRow[] = [
|
|
43
|
+
{
|
|
44
|
+
name: "pikachu",
|
|
45
|
+
pokemon_name: "Pikachu",
|
|
46
|
+
poke_id: 25,
|
|
47
|
+
height: 4,
|
|
48
|
+
sprite_front: "/files/25.png",
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
function renderGrid() {
|
|
53
|
+
return renderWithProviders(
|
|
54
|
+
<ReportGrid
|
|
55
|
+
columns={columns}
|
|
56
|
+
rows={buildGridRows(rows, 0, 20)}
|
|
57
|
+
sort={{ field: "modified", order: "desc" }}
|
|
58
|
+
onSortChange={vi.fn()}
|
|
59
|
+
columnFilters={{}}
|
|
60
|
+
onColumnFilterChange={vi.fn()}
|
|
61
|
+
totals={[]}
|
|
62
|
+
imageField="sprite_front"
|
|
63
|
+
titleField="pokemon_name"
|
|
64
|
+
fileUrl={(path) => `https://site.test${path}`}
|
|
65
|
+
/>,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
describe("ReportGrid on a phone", () => {
|
|
70
|
+
const width = window.innerWidth;
|
|
71
|
+
beforeEach(() => {
|
|
72
|
+
window.innerWidth = 390;
|
|
73
|
+
});
|
|
74
|
+
afterEach(() => {
|
|
75
|
+
window.innerWidth = width;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("drops the row numbers and the per-column filter row", () => {
|
|
79
|
+
renderGrid();
|
|
80
|
+
expect(screen.queryByLabelText("Row number")).toBeNull();
|
|
81
|
+
expect(screen.queryByLabelText("Filter Pokemon Name")).toBeNull();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("heads a number column without its unit and puts the unit in the cell", () => {
|
|
85
|
+
renderGrid();
|
|
86
|
+
expect(screen.getByText("Height")).toBeTruthy();
|
|
87
|
+
expect(screen.queryByText("Height (dm)")).toBeNull();
|
|
88
|
+
const cells = Array.from(document.querySelectorAll("span, div")).filter(
|
|
89
|
+
(node) => node.textContent === "4 dm" && node.children.length > 0,
|
|
90
|
+
);
|
|
91
|
+
expect(cells.length).toBeGreaterThan(0);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("leads the title cell with the record's image", () => {
|
|
95
|
+
const { container } = renderGrid();
|
|
96
|
+
const image = container.querySelector('img[src="https://site.test/files/25.png"]');
|
|
97
|
+
expect(image).not.toBeNull();
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe("reportColumnUnit", () => {
|
|
102
|
+
it("splits a short trailing unit off a number label only", () => {
|
|
103
|
+
expect(reportColumnUnit(columns[2])).toEqual({ label: "Height", unit: "dm" });
|
|
104
|
+
expect(reportColumnUnit({ ...columns[0], label: "Name (EN)" })).toEqual({ label: "Name (EN)" });
|
|
105
|
+
expect(
|
|
106
|
+
reportColumnUnit({ ...columns[2], fieldtype: "Currency", label: "Price (USD)" }),
|
|
107
|
+
).toEqual({ label: "Price (USD)" });
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe("phoneReportFieldnames", () => {
|
|
112
|
+
it("picks the title, then numbers, up to four columns", () => {
|
|
113
|
+
expect(
|
|
114
|
+
phoneReportFieldnames({
|
|
115
|
+
title_field: "pokemon_name",
|
|
116
|
+
image_field: "sprite_front",
|
|
117
|
+
fields: [
|
|
118
|
+
{ fieldname: "pokemon_name", fieldtype: "Data", label: "Pokemon Name", in_list_view: 1 },
|
|
119
|
+
{ fieldname: "poke_id", fieldtype: "Int", label: "Poke ID", in_list_view: 1 },
|
|
120
|
+
{ fieldname: "species", fieldtype: "Data", label: "Species", in_list_view: 1 },
|
|
121
|
+
{ fieldname: "height", fieldtype: "Int", label: "Height (dm)" },
|
|
122
|
+
{ fieldname: "weight", fieldtype: "Int", label: "Weight (hg)" },
|
|
123
|
+
{ fieldname: "base_experience", fieldtype: "Int", label: "Base Experience" },
|
|
124
|
+
{ fieldname: "sprite_front", fieldtype: "Attach Image", label: "Sprite (Front)" },
|
|
125
|
+
],
|
|
126
|
+
}),
|
|
127
|
+
).toEqual(["pokemon_name", "poke_id", "height", "weight"]);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("falls back to flagged text fields when there are too few numbers", () => {
|
|
131
|
+
expect(
|
|
132
|
+
phoneReportFieldnames({
|
|
133
|
+
fields: [
|
|
134
|
+
{ fieldname: "subject", fieldtype: "Data", label: "Subject", in_list_view: 1 },
|
|
135
|
+
{ fieldname: "status", fieldtype: "Select", label: "Status", in_list_view: 1 },
|
|
136
|
+
],
|
|
137
|
+
}),
|
|
138
|
+
).toEqual(["name", "subject", "status"]);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -8,11 +8,21 @@
|
|
|
8
8
|
// scrollport and wraps header content in Text, so it cannot host an input
|
|
9
9
|
// row under its headers today — capability gap flagged to the table
|
|
10
10
|
// workstream; this grid is the interim desk-parity surface.
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
Image,
|
|
13
|
+
ScrollView,
|
|
14
|
+
Text,
|
|
15
|
+
Tooltip,
|
|
16
|
+
View,
|
|
17
|
+
XStack,
|
|
18
|
+
getTokenValue,
|
|
19
|
+
isWeb,
|
|
20
|
+
} from "@multiplatform.one/components";
|
|
12
21
|
import { Input } from "@multiplatform.one/forms";
|
|
13
|
-
import { Table } from "@multiplatform.one/table";
|
|
22
|
+
import { Table, resolveSimpleTableColumns } from "@multiplatform.one/table";
|
|
14
23
|
import {
|
|
15
24
|
ensureFocusVisibleRing,
|
|
25
|
+
useLayoutSizeClass,
|
|
16
26
|
formatCurrency,
|
|
17
27
|
formatNumber,
|
|
18
28
|
formatPercent,
|
|
@@ -20,6 +30,7 @@ import {
|
|
|
20
30
|
import { CaretDownIcon, CaretUpDownIcon } from "@phosphor-icons/react";
|
|
21
31
|
import React, { useCallback, useState } from "react";
|
|
22
32
|
import { useTranslation } from "react-i18next";
|
|
33
|
+
import { splitUnitLabel } from "../fieldRegistry";
|
|
23
34
|
import { formatDateCell } from "../list/listFormat";
|
|
24
35
|
import {
|
|
25
36
|
REPORT_NUMERIC_FIELDTYPES,
|
|
@@ -32,6 +43,42 @@ import {
|
|
|
32
43
|
|
|
33
44
|
const ROW_NUMBER_WIDTH = 56;
|
|
34
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Phone widths from the table package's column fitter (FZ-7698): numbers
|
|
48
|
+
* take a narrow token width, the title column takes what is left, so four
|
|
49
|
+
* columns fit without a sideways scroll.
|
|
50
|
+
*/
|
|
51
|
+
function fitColumns(
|
|
52
|
+
columns: readonly ReportColumnDef[],
|
|
53
|
+
rows: readonly ReportGridRow[],
|
|
54
|
+
titleFieldname: string | undefined,
|
|
55
|
+
viewportWidth: number,
|
|
56
|
+
): ReportColumnDef[] {
|
|
57
|
+
if (viewportWidth <= 0) return [...columns];
|
|
58
|
+
const data = rows.flatMap((row) => (row.kind === "row" ? [row.row] : []));
|
|
59
|
+
const fitted = resolveSimpleTableColumns(
|
|
60
|
+
columns.map((column) => ({
|
|
61
|
+
accessorKey: column.fieldname,
|
|
62
|
+
header: column.label,
|
|
63
|
+
type: REPORT_NUMERIC_FIELDTYPES.has(column.fieldtype)
|
|
64
|
+
? ("number" as const)
|
|
65
|
+
: ("text" as const),
|
|
66
|
+
...(column.fieldname === titleFieldname
|
|
67
|
+
? { width: { flex: 1, min: "$10" as const } }
|
|
68
|
+
: REPORT_NUMERIC_FIELDTYPES.has(column.fieldtype)
|
|
69
|
+
? { width: "$8" as const }
|
|
70
|
+
: { width: "$11" as const }),
|
|
71
|
+
})),
|
|
72
|
+
data as Array<Record<string, any>>,
|
|
73
|
+
viewportWidth,
|
|
74
|
+
(token) => Number(getTokenValue(token as Parameters<typeof getTokenValue>[0], "size")),
|
|
75
|
+
);
|
|
76
|
+
return columns.map((column, index) => ({
|
|
77
|
+
...column,
|
|
78
|
+
width: fitted[index]?.width ?? column.width,
|
|
79
|
+
}));
|
|
80
|
+
}
|
|
81
|
+
|
|
35
82
|
// ── LC-49/LC-50 row keyboard contract ───────────────────────────────
|
|
36
83
|
// Local mirror of the table package's `isInteractiveInnerTarget`
|
|
37
84
|
// (public/table/src/utils/rowPress.ts — internal util, not on the
|
|
@@ -92,6 +139,29 @@ export interface ReportGridProps {
|
|
|
92
139
|
totals: readonly ReportTotalsCell[];
|
|
93
140
|
/** Group-by column (group header rows span the grid). */
|
|
94
141
|
groupByColumn?: ReportColumnDef;
|
|
142
|
+
/**
|
|
143
|
+
* The doctype's image field. On a phone the title cell leads with that
|
|
144
|
+
* image, so the record reads as one column instead of two.
|
|
145
|
+
*/
|
|
146
|
+
imageField?: string;
|
|
147
|
+
/** The doctype's title field (defaults to the first column). */
|
|
148
|
+
titleField?: string;
|
|
149
|
+
/** Turns a stored file path into a URL the image can load. */
|
|
150
|
+
fileUrl?: (path: string) => string;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* A number column's label and unit: "Height (dm)" heads the column as
|
|
155
|
+
* "Height" and each cell reads "3 dm", so the unit is never lost to a
|
|
156
|
+
* narrow header. Currency and percent carry their own symbol.
|
|
157
|
+
*/
|
|
158
|
+
export function reportColumnUnit(column: ReportColumnDef): { label: string; unit?: string } {
|
|
159
|
+
if (!REPORT_NUMERIC_FIELDTYPES.has(column.fieldtype)) return { label: column.label };
|
|
160
|
+
if (column.fieldtype === "Currency" || column.fieldtype === "Percent") {
|
|
161
|
+
return { label: column.label };
|
|
162
|
+
}
|
|
163
|
+
const split = splitUnitLabel(column.label);
|
|
164
|
+
return { label: String(split.label), unit: split.unit };
|
|
95
165
|
}
|
|
96
166
|
|
|
97
167
|
function alignToJustify(align: ReportColumnDef["align"]) {
|
|
@@ -116,10 +186,12 @@ function ReportCellValue({
|
|
|
116
186
|
column,
|
|
117
187
|
row,
|
|
118
188
|
onOpenLink,
|
|
189
|
+
fileUrl,
|
|
119
190
|
}: {
|
|
120
191
|
column: ReportColumnDef;
|
|
121
192
|
row: ReportRow;
|
|
122
193
|
onOpenLink?: (doctype: string, name: string) => void;
|
|
194
|
+
fileUrl?: (path: string) => string;
|
|
123
195
|
}) {
|
|
124
196
|
const raw = row[column.fieldname];
|
|
125
197
|
if (raw === null || raw === undefined || raw === "") {
|
|
@@ -138,12 +210,18 @@ function ReportCellValue({
|
|
|
138
210
|
}
|
|
139
211
|
if (REPORT_NUMERIC_FIELDTYPES.has(column.fieldtype)) {
|
|
140
212
|
const numeric = Number(raw);
|
|
213
|
+
const { unit } = reportColumnUnit(column);
|
|
141
214
|
return (
|
|
142
|
-
<Text fontSize="$3" color="$color12" fontFamily="$mono">
|
|
215
|
+
<Text fontSize="$3" color="$color12" fontFamily="$mono" numberOfLines={1}>
|
|
143
216
|
{Number.isFinite(numeric) ? formatNumericCell(numeric, column.fieldtype) : String(raw)}
|
|
217
|
+
{unit ? <Text color="$color10">{` ${unit}`}</Text> : null}
|
|
144
218
|
</Text>
|
|
145
219
|
);
|
|
146
220
|
}
|
|
221
|
+
if (column.fieldtype === "Attach Image" && typeof raw === "string") {
|
|
222
|
+
// Desk prints the path (formatters.js AttachImage); a report reads the image.
|
|
223
|
+
return <Image src={fileUrl ? fileUrl(raw) : raw} alt="" size="extraSmall" framed />;
|
|
224
|
+
}
|
|
147
225
|
const value = String(raw);
|
|
148
226
|
switch (column.fieldtype) {
|
|
149
227
|
case "Date":
|
|
@@ -189,7 +267,7 @@ function ReportCellValue({
|
|
|
189
267
|
}
|
|
190
268
|
|
|
191
269
|
export function ReportGrid({
|
|
192
|
-
columns,
|
|
270
|
+
columns: columnsIn,
|
|
193
271
|
rows,
|
|
194
272
|
sort,
|
|
195
273
|
onSortChange,
|
|
@@ -199,8 +277,19 @@ export function ReportGrid({
|
|
|
199
277
|
onOpenLink,
|
|
200
278
|
totals,
|
|
201
279
|
groupByColumn,
|
|
280
|
+
imageField,
|
|
281
|
+
titleField,
|
|
282
|
+
fileUrl,
|
|
202
283
|
}: ReportGridProps) {
|
|
203
284
|
const { t } = useTranslation();
|
|
285
|
+
// A phone report fits its columns to the screen: no row numbers, no
|
|
286
|
+
// per-column filter row (the filter bar above owns filtering there), the
|
|
287
|
+
// title column takes the room the numbers leave (FZ-7684).
|
|
288
|
+
const compact = useLayoutSizeClass() === "compact";
|
|
289
|
+
const [viewportWidth, setViewportWidth] = useState(0);
|
|
290
|
+
const titleFieldname = titleField ?? columnsIn[0]?.fieldname;
|
|
291
|
+
const columns = compact ? fitColumns(columnsIn, rows, titleFieldname, viewportWidth) : columnsIn;
|
|
292
|
+
const rowNumberWidth = compact ? 0 : ROW_NUMBER_WIDTH;
|
|
204
293
|
|
|
205
294
|
const handleSortPress = useCallback(
|
|
206
295
|
(fieldname: string) => {
|
|
@@ -310,10 +399,19 @@ export function ReportGrid({
|
|
|
310
399
|
);
|
|
311
400
|
|
|
312
401
|
const totalsByField = new Map(totals.map((total) => [total.fieldname, total]));
|
|
313
|
-
const gridWidth =
|
|
402
|
+
const gridWidth = rowNumberWidth + columns.reduce((sum, column) => sum + column.width, 0);
|
|
314
403
|
|
|
315
404
|
return (
|
|
316
|
-
<ScrollView
|
|
405
|
+
<ScrollView
|
|
406
|
+
horizontal
|
|
407
|
+
showsHorizontalScrollIndicator
|
|
408
|
+
flexGrow={0}
|
|
409
|
+
flexShrink={1}
|
|
410
|
+
onLayout={(event) => {
|
|
411
|
+
const width = Math.floor(event.nativeEvent.layout.width);
|
|
412
|
+
setViewportWidth((previous) => (previous === width ? previous : width));
|
|
413
|
+
}}
|
|
414
|
+
>
|
|
317
415
|
<Table
|
|
318
416
|
minWidth={gridWidth}
|
|
319
417
|
width={gridWidth}
|
|
@@ -326,14 +424,16 @@ export function ReportGrid({
|
|
|
326
424
|
<Table.Head>
|
|
327
425
|
{/* Header row: label + sort affordance (aria-sort, Enter/Space). */}
|
|
328
426
|
<Table.Row>
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
427
|
+
{compact ? null : (
|
|
428
|
+
<Table.HeaderCell
|
|
429
|
+
cellWidth={ROW_NUMBER_WIDTH}
|
|
430
|
+
aria-label={t("report.rowNumber", "Row number")}
|
|
431
|
+
>
|
|
432
|
+
<Text fontSize="$2" color="$color9">
|
|
433
|
+
#
|
|
434
|
+
</Text>
|
|
435
|
+
</Table.HeaderCell>
|
|
436
|
+
)}
|
|
337
437
|
{columns.map((column) => {
|
|
338
438
|
const sorted = sort.field === column.fieldname ? sort.order : undefined;
|
|
339
439
|
return (
|
|
@@ -377,7 +477,7 @@ export function ReportGrid({
|
|
|
377
477
|
hoverStyle={{ opacity: 0.8 }}
|
|
378
478
|
>
|
|
379
479
|
<Text fontSize="$3" fontWeight="400" numberOfLines={1}>
|
|
380
|
-
{column.label}
|
|
480
|
+
{reportColumnUnit(column).label}
|
|
381
481
|
</Text>
|
|
382
482
|
{/* Caret orientation mirrors DataTable's headers:
|
|
383
483
|
asc = caret-down, desc = rotated 180. */}
|
|
@@ -400,42 +500,44 @@ export function ReportGrid({
|
|
|
400
500
|
</Table.Row>
|
|
401
501
|
{/* Filter row — desk parity: one input per column, standard filter
|
|
402
502
|
pipeline. Number/date accept comparison prefixes (>, <, >=…). */}
|
|
403
|
-
|
|
404
|
-
<Table.
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
<
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
503
|
+
{compact ? null : (
|
|
504
|
+
<Table.Row>
|
|
505
|
+
<Table.HeaderCell cellWidth={ROW_NUMBER_WIDTH} paddingVertical="$1.5" />
|
|
506
|
+
{columns.map((column) => {
|
|
507
|
+
const placeholder =
|
|
508
|
+
column.kind === "number" || column.kind === "date"
|
|
509
|
+
? t("report.filterComparablePlaceholder", "= > <…")
|
|
510
|
+
: column.kind === "check"
|
|
511
|
+
? t("report.filterCheckPlaceholder", "yes / no")
|
|
512
|
+
: t("report.filterPlaceholder", "Filter…");
|
|
513
|
+
return (
|
|
514
|
+
<Table.HeaderCell
|
|
515
|
+
key={column.fieldname}
|
|
516
|
+
cellWidth={column.width}
|
|
517
|
+
paddingVertical="$1.5"
|
|
518
|
+
overflow="visible"
|
|
519
|
+
>
|
|
520
|
+
<View flex={1} minWidth={0} overflow="visible">
|
|
521
|
+
<Input
|
|
522
|
+
size="$2"
|
|
523
|
+
value={columnFilters[column.fieldname] ?? ""}
|
|
524
|
+
onChangeText={(next) => onColumnFilterChange(column.fieldname, next)}
|
|
525
|
+
placeholder={placeholder}
|
|
526
|
+
// inputProps is the eject that reaches the real input
|
|
527
|
+
// element — a top-level aria-label stays on the wrapper.
|
|
528
|
+
inputProps={{
|
|
529
|
+
"aria-label": t("report.filterColumn", {
|
|
530
|
+
defaultValue: "Filter {{column}}",
|
|
531
|
+
column: column.label,
|
|
532
|
+
}),
|
|
533
|
+
}}
|
|
534
|
+
/>
|
|
535
|
+
</View>
|
|
536
|
+
</Table.HeaderCell>
|
|
537
|
+
);
|
|
538
|
+
})}
|
|
539
|
+
</Table.Row>
|
|
540
|
+
)}
|
|
439
541
|
</Table.Head>
|
|
440
542
|
<Table.Body>
|
|
441
543
|
{(() => {
|
|
@@ -475,21 +577,51 @@ export function ReportGrid({
|
|
|
475
577
|
? (getRowPressProps(gridRow.row, pressCursor) as Record<string, unknown>)
|
|
476
578
|
: { "aria-label": gridRow.row.name })}
|
|
477
579
|
>
|
|
478
|
-
|
|
479
|
-
<
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
{columns.map((column) => (
|
|
484
|
-
<Table.Cell
|
|
485
|
-
key={column.fieldname}
|
|
486
|
-
cellWidth={column.width}
|
|
487
|
-
paddingVertical="$2"
|
|
488
|
-
justifyContent={alignToJustify(column.align)}
|
|
489
|
-
>
|
|
490
|
-
<ReportCellValue column={column} row={gridRow.row} onOpenLink={onOpenLink} />
|
|
580
|
+
{compact ? null : (
|
|
581
|
+
<Table.Cell cellWidth={ROW_NUMBER_WIDTH} paddingVertical="$2">
|
|
582
|
+
<Text fontSize="$2" color="$color9" fontFamily="$mono">
|
|
583
|
+
{gridRow.index}
|
|
584
|
+
</Text>
|
|
491
585
|
</Table.Cell>
|
|
492
|
-
)
|
|
586
|
+
)}
|
|
587
|
+
{columns.map((column) => {
|
|
588
|
+
const image =
|
|
589
|
+
compact && imageField && column.fieldname === titleFieldname
|
|
590
|
+
? gridRow.row[imageField]
|
|
591
|
+
: undefined;
|
|
592
|
+
return (
|
|
593
|
+
<Table.Cell
|
|
594
|
+
key={column.fieldname}
|
|
595
|
+
cellWidth={column.width}
|
|
596
|
+
paddingVertical="$2"
|
|
597
|
+
justifyContent={alignToJustify(column.align)}
|
|
598
|
+
>
|
|
599
|
+
{typeof image === "string" && image ? (
|
|
600
|
+
<XStack gap="$2" alignItems="center" minWidth="$0" flexShrink={1}>
|
|
601
|
+
<Image
|
|
602
|
+
src={fileUrl ? fileUrl(image) : image}
|
|
603
|
+
alt=""
|
|
604
|
+
size="extraSmall"
|
|
605
|
+
framed
|
|
606
|
+
/>
|
|
607
|
+
<ReportCellValue
|
|
608
|
+
column={column}
|
|
609
|
+
row={gridRow.row}
|
|
610
|
+
onOpenLink={onOpenLink}
|
|
611
|
+
fileUrl={fileUrl}
|
|
612
|
+
/>
|
|
613
|
+
</XStack>
|
|
614
|
+
) : (
|
|
615
|
+
<ReportCellValue
|
|
616
|
+
column={column}
|
|
617
|
+
row={gridRow.row}
|
|
618
|
+
onOpenLink={onOpenLink}
|
|
619
|
+
fileUrl={fileUrl}
|
|
620
|
+
/>
|
|
621
|
+
)}
|
|
622
|
+
</Table.Cell>
|
|
623
|
+
);
|
|
624
|
+
})}
|
|
493
625
|
</Table.Row>
|
|
494
626
|
);
|
|
495
627
|
});
|
|
@@ -498,13 +630,28 @@ export function ReportGrid({
|
|
|
498
630
|
{totals.length > 0 && (
|
|
499
631
|
<Table.Foot borderTopWidth={1} borderTopColor="$color6">
|
|
500
632
|
<Table.Row backgroundColor="$color2">
|
|
501
|
-
|
|
502
|
-
<
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
633
|
+
{compact ? null : (
|
|
634
|
+
<Table.Cell cellWidth={ROW_NUMBER_WIDTH} paddingVertical="$2">
|
|
635
|
+
<Text fontSize="$2" fontWeight="400" color="$color10">
|
|
636
|
+
{t("report.totals", "Totals")}
|
|
637
|
+
</Text>
|
|
638
|
+
</Table.Cell>
|
|
639
|
+
)}
|
|
640
|
+
{columns.map((column, columnIndex) => {
|
|
507
641
|
const total = totalsByField.get(column.fieldname);
|
|
642
|
+
if (compact && columnIndex === 0 && !total) {
|
|
643
|
+
return (
|
|
644
|
+
<Table.Cell
|
|
645
|
+
key={column.fieldname}
|
|
646
|
+
cellWidth={column.width}
|
|
647
|
+
paddingVertical="$2"
|
|
648
|
+
>
|
|
649
|
+
<Text fontSize="$2" fontWeight="400" color="$color10">
|
|
650
|
+
{t("report.totals", "Totals")}
|
|
651
|
+
</Text>
|
|
652
|
+
</Table.Cell>
|
|
653
|
+
);
|
|
654
|
+
}
|
|
508
655
|
return (
|
|
509
656
|
<Table.Cell
|
|
510
657
|
key={column.fieldname}
|
|
@@ -108,7 +108,7 @@ export function ReportViewToolbar({
|
|
|
108
108
|
<DropdownMenu.Trigger asChild>
|
|
109
109
|
<Button size="$3" chromeless icon={ColumnsIcon}>
|
|
110
110
|
{t("report.columns", {
|
|
111
|
-
defaultValue: "Columns
|
|
111
|
+
defaultValue: "Columns · {{enabled}} of {{total}}",
|
|
112
112
|
enabled: selected.length,
|
|
113
113
|
total: columns.length,
|
|
114
114
|
})}
|
|
@@ -224,6 +224,44 @@ export function defaultReportFieldnames(meta?: ReportMetaSource): string[] {
|
|
|
224
224
|
return out;
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
+
/** Columns a phone report shows before the reader picks more (FZ-7684). */
|
|
228
|
+
export const PHONE_REPORT_COLUMNS = 4;
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* A phone report's default columns: the title (the record, with its image
|
|
232
|
+
* drawn inside the title cell), then numbers, which stay narrow enough for
|
|
233
|
+
* four columns to fit a phone. Flagged list fields lead the numbers; text
|
|
234
|
+
* columns join only when there are too few numbers. Everything else is one
|
|
235
|
+
* tap away in the column picker.
|
|
236
|
+
*/
|
|
237
|
+
export function phoneReportFieldnames(
|
|
238
|
+
meta?: ReportMetaSource,
|
|
239
|
+
max: number = PHONE_REPORT_COLUMNS,
|
|
240
|
+
): string[] {
|
|
241
|
+
const out: string[] = [];
|
|
242
|
+
const push = (fieldname?: string) => {
|
|
243
|
+
if (fieldname && !out.includes(fieldname) && out.length < max) out.push(fieldname);
|
|
244
|
+
};
|
|
245
|
+
push(meta?.title_field || "name");
|
|
246
|
+
const values = (meta?.fields ?? []).filter((field) => {
|
|
247
|
+
const fieldtype = String(field.fieldtype);
|
|
248
|
+
return (
|
|
249
|
+
!VALUELESS_FIELDTYPES.has(fieldtype) &&
|
|
250
|
+
!fieldtype.startsWith("Table") &&
|
|
251
|
+
field.report_hide !== 1 &&
|
|
252
|
+
field.fieldname !== meta?.image_field &&
|
|
253
|
+
fieldtype !== "Attach Image"
|
|
254
|
+
);
|
|
255
|
+
});
|
|
256
|
+
const flagged = (field: DocFieldMeta) =>
|
|
257
|
+
field.in_list_view === 1 || field.in_standard_filter === 1;
|
|
258
|
+
const numeric = (field: DocFieldMeta) => REPORT_NUMERIC_FIELDTYPES.has(String(field.fieldtype));
|
|
259
|
+
for (const field of values) if (flagged(field) && numeric(field)) push(field.fieldname);
|
|
260
|
+
for (const field of values) if (numeric(field)) push(field.fieldname);
|
|
261
|
+
for (const field of values) if (flagged(field)) push(field.fieldname);
|
|
262
|
+
return out;
|
|
263
|
+
}
|
|
264
|
+
|
|
227
265
|
/** Resolve selected fieldnames against the pickable set (drops unknowns). */
|
|
228
266
|
export function resolveSelectedColumns(
|
|
229
267
|
all: ReportColumnDef[],
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FrappeReportBuilder.d.ts","sourceRoot":"","sources":["../src/FrappeReportBuilder.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,OAAO,EAOL,KAAK,aAAa,EAClB,KAAK,YAAY,EAClB,MAAM,+BAA+B,CAAC;AAavC,OAAc,EAA6C,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAEzF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"FrappeReportBuilder.d.ts","sourceRoot":"","sources":["../src/FrappeReportBuilder.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,OAAO,EAOL,KAAK,aAAa,EAClB,KAAK,YAAY,EAClB,MAAM,+BAA+B,CAAC;AAavC,OAAc,EAA6C,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAEzF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AA2B3C,KAAK,WAAW,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;AAE7C,MAAM,WAAW,wBAAyB,SAAQ,IAAI,CAAC,WAAW,EAAE,UAAU,CAAC;IAC7E,8DAA8D;IAC9D,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,WAAW,EAAE,CAAC;IAClD;;;OAGG;IACH,YAAY,CAAC,EAAE,WAAW,EAAE,CAAC;IAC7B,qFAAqF;IACrF,cAAc,CAAC,EAAE,MAAM,IAAI,CAAC;IAC5B;;;OAGG;IACH,cAAc,CAAC,EAAE,aAAa,CAAC,MAAM,GAAG,YAAY,CAAC,CAAC;IACtD,oFAAoF;IACpF,cAAc,CAAC,EAAE,MAAM,GAAG,aAAa,CAAC;IACxC,2EAA2E;IAC3E,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,6BAA6B;IAC7B,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,yEAAyE;IACzE,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnE,0CAA0C;IAC1C,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACrD;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IACjD,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,KAAK,IAAI,CAAC;IACrD,4CAA4C;IAC5C,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,oDAAoD;IACpD,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,SAAS,CAAC;IACpC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7D,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACvC;AAqID,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,wBAAwB,2CAMlE;AAwpBD,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC"}
|
|
@@ -15,6 +15,24 @@ export interface ReportGridProps {
|
|
|
15
15
|
totals: readonly ReportTotalsCell[];
|
|
16
16
|
/** Group-by column (group header rows span the grid). */
|
|
17
17
|
groupByColumn?: ReportColumnDef;
|
|
18
|
+
/**
|
|
19
|
+
* The doctype's image field. On a phone the title cell leads with that
|
|
20
|
+
* image, so the record reads as one column instead of two.
|
|
21
|
+
*/
|
|
22
|
+
imageField?: string;
|
|
23
|
+
/** The doctype's title field (defaults to the first column). */
|
|
24
|
+
titleField?: string;
|
|
25
|
+
/** Turns a stored file path into a URL the image can load. */
|
|
26
|
+
fileUrl?: (path: string) => string;
|
|
18
27
|
}
|
|
19
|
-
|
|
28
|
+
/**
|
|
29
|
+
* A number column's label and unit: "Height (dm)" heads the column as
|
|
30
|
+
* "Height" and each cell reads "3 dm", so the unit is never lost to a
|
|
31
|
+
* narrow header. Currency and percent carry their own symbol.
|
|
32
|
+
*/
|
|
33
|
+
export declare function reportColumnUnit(column: ReportColumnDef): {
|
|
34
|
+
label: string;
|
|
35
|
+
unit?: string;
|
|
36
|
+
};
|
|
37
|
+
export declare function ReportGrid({ columns: columnsIn, rows, sort, onSortChange, columnFilters, onColumnFilterChange, onOpenRow, onOpenLink, totals, groupByColumn, imageField, titleField, fileUrl, }: ReportGridProps): import("react/jsx-runtime").JSX.Element;
|
|
20
38
|
//# sourceMappingURL=ReportGrid.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ReportGrid.d.ts","sourceRoot":"","sources":["../../src/report/ReportGrid.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ReportGrid.d.ts","sourceRoot":"","sources":["../../src/report/ReportGrid.tsx"],"names":[],"mappings":"AAkCA,OAAO,EAEL,KAAK,eAAe,EACpB,KAAK,aAAa,EAElB,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACtB,MAAM,mBAAmB,CAAC;AAoF3B,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,SAAS,eAAe,EAAE,CAAC;IACpC,IAAI,EAAE,SAAS,aAAa,EAAE,CAAC;IAC/B,IAAI,EAAE,eAAe,CAAC;IACtB,YAAY,EAAE,CAAC,IAAI,EAAE,eAAe,KAAK,IAAI,CAAC;IAC9C,+DAA+D;IAC/D,aAAa,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAChD,oBAAoB,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACjE,oEAAoE;IACpE,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,0CAA0C;IAC1C,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACrD,uEAAuE;IACvE,MAAM,EAAE,SAAS,gBAAgB,EAAE,CAAC;IACpC,yDAAyD;IACzD,aAAa,CAAC,EAAE,eAAe,CAAC;IAChC;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gEAAgE;IAChE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,8DAA8D;IAC9D,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CACpC;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,eAAe,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAO1F;AAwGD,wBAAgB,UAAU,CAAC,EACzB,OAAO,EAAE,SAAS,EAClB,IAAI,EACJ,IAAI,EACJ,YAAY,EACZ,aAAa,EACb,oBAAoB,EACpB,SAAS,EACT,UAAU,EACV,MAAM,EACN,aAAa,EACb,UAAU,EACV,UAAU,EACV,OAAO,GACR,EAAE,eAAe,2CAyYjB"}
|
|
@@ -52,6 +52,16 @@ export declare function reportColumnsFromMeta(meta?: ReportMetaSource): ReportCo
|
|
|
52
52
|
* bundles and cannot be derived from the meta channel — honest subset.
|
|
53
53
|
*/
|
|
54
54
|
export declare function defaultReportFieldnames(meta?: ReportMetaSource): string[];
|
|
55
|
+
/** Columns a phone report shows before the reader picks more (FZ-7684). */
|
|
56
|
+
export declare const PHONE_REPORT_COLUMNS = 4;
|
|
57
|
+
/**
|
|
58
|
+
* A phone report's default columns: the title (the record, with its image
|
|
59
|
+
* drawn inside the title cell), then numbers, which stay narrow enough for
|
|
60
|
+
* four columns to fit a phone. Flagged list fields lead the numbers; text
|
|
61
|
+
* columns join only when there are too few numbers. Everything else is one
|
|
62
|
+
* tap away in the column picker.
|
|
63
|
+
*/
|
|
64
|
+
export declare function phoneReportFieldnames(meta?: ReportMetaSource, max?: number): string[];
|
|
55
65
|
/** Resolve selected fieldnames against the pickable set (drops unknowns). */
|
|
56
66
|
export declare function resolveSelectedColumns(all: ReportColumnDef[], fieldnames: readonly string[]): ReportColumnDef[];
|
|
57
67
|
/**
|