@protobase/ui 0.19.6 → 0.19.7
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/app/export/builder-port.js +54 -0
- package/dist/app/export/builder.js +61 -0
- package/dist/app/export/cells.js +93 -0
- package/dist/app/export/csv.js +17 -0
- package/dist/app/export/export-dialog.js +235 -0
- package/dist/app/export/export-options.js +49 -0
- package/dist/app/export/export-worker.d.ts +1 -0
- package/dist/app/export/export-worker.js +9 -0
- package/dist/app/export/run-export.js +62 -0
- package/dist/app/export/sink.js +91 -0
- package/dist/app/export/use-export-flow.js +139 -0
- package/dist/app/export/xlsx.js +91 -0
- package/dist/app/list-page.js +28 -3
- package/dist/data/query-keys.js +7 -0
- package/dist/package.js +1 -1
- package/dist/shell/assets/app-BRVJlz_E.js +68 -0
- package/dist/shell/assets/export-worker-CVd5saLq.js +2 -0
- package/dist/shell/assets/{index-Dqgcyg7M.js → index-CafsxSWx.js} +1 -1
- package/dist/shell/assets/index-Cuulz26c.css +2 -0
- package/dist/shell/assets/maplibre-Cb7ks8SG.js +814 -0
- package/dist/shell/assets/{record-map-m5K2uttt.js → record-map-dXyPrmGg.js} +1 -1
- package/dist/shell/assets/{records-map-CA_JG9RR.js → records-map-bGH18FRn.js} +1 -1
- package/dist/shell/assets/{shared-protobase-ui-BBqKlVLh.js → shared-protobase-ui-e-G14mxb.js} +1 -1
- package/dist/shell/index.html +4 -4
- package/dist/shell/protobase.shell.json +1 -1
- package/package.json +5 -4
- package/dist/shell/assets/app-DwKzLvfm.js +0 -67
- package/dist/shell/assets/index-CwhnZIJp.css +0 -2
- package/dist/shell/assets/maplibre-BWjNhvrH.js +0 -814
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { formatInt } from "../../format/number.js";
|
|
2
|
+
import { keys } from "../../data/query-keys.js";
|
|
3
|
+
import { useClient } from "../../data/api-provider.js";
|
|
4
|
+
import { useToast } from "../../toasts/toast-provider.js";
|
|
5
|
+
import { listLabels } from "../../live/relation-labels.js";
|
|
6
|
+
import { runExport } from "./run-export.js";
|
|
7
|
+
import { defaultExportColumns, exportColumns, exportHeaders, exportLimit, exportableFields, exportsSelection, selectionFilters } from "./export-options.js";
|
|
8
|
+
import { createWorkerPort } from "./builder-port.js";
|
|
9
|
+
import { ExportDialog, ExportProgressDialog, countedRows } from "./export-dialog.js";
|
|
10
|
+
import { canSaveAsBuilt, createDownloadSink, exportFileName, pickFileSink } from "./sink.js";
|
|
11
|
+
import { useRef, useState } from "react";
|
|
12
|
+
import { isApiError } from "@protobase/client";
|
|
13
|
+
import { jsx } from "react/jsx-runtime";
|
|
14
|
+
import { useQuery } from "@tanstack/react-query";
|
|
15
|
+
//#region src/app/export/use-export-flow.tsx
|
|
16
|
+
const defaultSink = (name, format) => canSaveAsBuilt() ? pickFileSink(name, format) : Promise.resolve(createDownloadSink(name, format));
|
|
17
|
+
const selectedKey = (model, row) => {
|
|
18
|
+
const value = row[model.primaryKey[0]];
|
|
19
|
+
return typeof value === "object" && value !== null && "id" in value ? value.id : value;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* The list's Export: `open` shows its settings, with the selected rows when there are some, and `dialogs` draws them and
|
|
23
|
+
* the progress while the file is built. Rows come through the list API as the user may read them; the file is built in
|
|
24
|
+
* a worker and written as it goes.
|
|
25
|
+
*/
|
|
26
|
+
const useExportFlow = ({ model, view, resources, views, plural, filter, orderBy, createPort = createWorkerPort, createSink = defaultSink }) => {
|
|
27
|
+
const client = useClient();
|
|
28
|
+
const toast = useToast();
|
|
29
|
+
const [stage, setStage] = useState({ kind: "closed" });
|
|
30
|
+
const abort = useRef(void 0);
|
|
31
|
+
const count = useQuery({
|
|
32
|
+
queryKey: keys.exportCount(model.name, filter),
|
|
33
|
+
enabled: stage.kind === "settings",
|
|
34
|
+
queryFn: async () => {
|
|
35
|
+
const page = await client.list(model.name, {
|
|
36
|
+
filter: filter || void 0,
|
|
37
|
+
pageSize: 1,
|
|
38
|
+
count: "exact",
|
|
39
|
+
fields: model.primaryKey
|
|
40
|
+
});
|
|
41
|
+
return page.totalSize === void 0 ? {
|
|
42
|
+
kind: "estimate",
|
|
43
|
+
rows: page.totalSizeEstimate
|
|
44
|
+
} : {
|
|
45
|
+
kind: "exact",
|
|
46
|
+
rows: page.totalSize
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}).data ?? { kind: "counting" };
|
|
50
|
+
const open = (rows = []) => setStage({
|
|
51
|
+
kind: "settings",
|
|
52
|
+
selected: exportsSelection(model) ? rows.map((row) => selectedKey(model, row)) : []
|
|
53
|
+
});
|
|
54
|
+
const start = async (settings, selected) => {
|
|
55
|
+
const name = exportFileName(plural, settings.format);
|
|
56
|
+
const sink = await createSink(name, settings.format);
|
|
57
|
+
if (!sink) return;
|
|
58
|
+
const limit = exportLimit(view, settings.format);
|
|
59
|
+
const rows = settings.scope === "selected" ? selected.length : countedRows(count);
|
|
60
|
+
const total = rows === void 0 ? void 0 : Math.min(rows, limit);
|
|
61
|
+
const controller = new AbortController();
|
|
62
|
+
abort.current = controller;
|
|
63
|
+
setStage({
|
|
64
|
+
kind: "running",
|
|
65
|
+
done: 0,
|
|
66
|
+
total
|
|
67
|
+
});
|
|
68
|
+
try {
|
|
69
|
+
const result = await runExport({
|
|
70
|
+
client,
|
|
71
|
+
resource: model.name,
|
|
72
|
+
filters: settings.scope === "selected" ? selectionFilters(model, selected) : [filter],
|
|
73
|
+
orderBy,
|
|
74
|
+
plan: {
|
|
75
|
+
format: settings.format,
|
|
76
|
+
mode: settings.mode,
|
|
77
|
+
name: plural,
|
|
78
|
+
columns: exportColumns(model, view, settings.columns)
|
|
79
|
+
},
|
|
80
|
+
labels: settings.mode === "shown" ? listLabels(resources, views, model, settings.columns) : {},
|
|
81
|
+
limit,
|
|
82
|
+
port: createPort(),
|
|
83
|
+
sink,
|
|
84
|
+
signal: controller.signal,
|
|
85
|
+
onProgress: (done) => controller.signal.aborted || setStage({
|
|
86
|
+
kind: "running",
|
|
87
|
+
done,
|
|
88
|
+
total
|
|
89
|
+
})
|
|
90
|
+
});
|
|
91
|
+
toast.show({
|
|
92
|
+
state: "success",
|
|
93
|
+
title: `Exported ${formatInt(result.rows)} ${plural.toLowerCase()}`,
|
|
94
|
+
description: result.truncated ? `${name}: the first ${formatInt(result.rows)} rows, as many as a file holds.` : name
|
|
95
|
+
});
|
|
96
|
+
} catch (error) {
|
|
97
|
+
if (error instanceof DOMException && error.name === "AbortError") return;
|
|
98
|
+
if (!isApiError(error)) throw error;
|
|
99
|
+
toast.show({
|
|
100
|
+
state: "error",
|
|
101
|
+
title: `Could not export ${plural.toLowerCase()}`,
|
|
102
|
+
description: error.message
|
|
103
|
+
});
|
|
104
|
+
} finally {
|
|
105
|
+
abort.current = void 0;
|
|
106
|
+
setStage((current) => current.kind === "running" ? { kind: "closed" } : current);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
const cancel = () => {
|
|
110
|
+
abort.current?.abort();
|
|
111
|
+
setStage({ kind: "closed" });
|
|
112
|
+
};
|
|
113
|
+
const fields = exportableFields(model);
|
|
114
|
+
const labels = exportHeaders(model, view, fields);
|
|
115
|
+
return {
|
|
116
|
+
open,
|
|
117
|
+
dialogs: stage.kind === "settings" ? /* @__PURE__ */ jsx(ExportDialog, {
|
|
118
|
+
plural,
|
|
119
|
+
view,
|
|
120
|
+
fields: fields.map((name) => ({
|
|
121
|
+
name,
|
|
122
|
+
label: labels[name]
|
|
123
|
+
})),
|
|
124
|
+
initialColumns: defaultExportColumns(model, view),
|
|
125
|
+
count,
|
|
126
|
+
selected: stage.selected.length > 0 ? stage.selected.length : void 0,
|
|
127
|
+
open: true,
|
|
128
|
+
onExport: (settings) => void start(settings, stage.selected),
|
|
129
|
+
onClose: () => setStage({ kind: "closed" })
|
|
130
|
+
}) : stage.kind === "running" ? /* @__PURE__ */ jsx(ExportProgressDialog, {
|
|
131
|
+
plural,
|
|
132
|
+
done: stage.done,
|
|
133
|
+
total: stage.total,
|
|
134
|
+
onCancel: cancel
|
|
135
|
+
}) : null
|
|
136
|
+
};
|
|
137
|
+
};
|
|
138
|
+
//#endregion
|
|
139
|
+
export { useExportFlow };
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Zip, ZipDeflate } from "fflate";
|
|
2
|
+
//#region src/app/export/xlsx.ts
|
|
3
|
+
/** The most data rows a sheet holds: Excel's 1,048,576 rows, less the header. */
|
|
4
|
+
const xlsxMaxRows = 1048575;
|
|
5
|
+
const maxCellText = 32767;
|
|
6
|
+
const invalidXml = /[\u0000-\u0008\u000b\u000c\u000e-\u001f]/g;
|
|
7
|
+
const escapeXml = (text) => text.replace(invalidXml, "").replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """);
|
|
8
|
+
/** A column's letters: 0 is A, 25 is Z, 26 is AA. */
|
|
9
|
+
const columnLetters = (index) => {
|
|
10
|
+
const letter = String.fromCharCode(65 + index % 26);
|
|
11
|
+
return index < 26 ? letter : columnLetters(Math.floor(index / 26) - 1) + letter;
|
|
12
|
+
};
|
|
13
|
+
const textCell = (ref, text, style = "") => `<c r="${ref}"${style} t="inlineStr"><is><t xml:space="preserve">${escapeXml(text.slice(0, maxCellText))}</t></is></c>`;
|
|
14
|
+
const xml = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n";
|
|
15
|
+
const mainNs = "http://schemas.openxmlformats.org/spreadsheetml/2006/main";
|
|
16
|
+
const relNs = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
|
|
17
|
+
const packageRelNs = "http://schemas.openxmlformats.org/package/2006/relationships";
|
|
18
|
+
const contentTypes = `${xml}<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/><Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/><Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/></Types>`;
|
|
19
|
+
const rootRels = `${xml}<Relationships xmlns="${packageRelNs}"><Relationship Id="rId1" Type="${relNs}/officeDocument" Target="xl/workbook.xml"/></Relationships>`;
|
|
20
|
+
const workbookRels = `${xml}<Relationships xmlns="${packageRelNs}"><Relationship Id="rId1" Type="${relNs}/worksheet" Target="worksheets/sheet1.xml"/><Relationship Id="rId2" Type="${relNs}/styles" Target="styles.xml"/></Relationships>`;
|
|
21
|
+
const sheetName = (name) => name.replace(/[:\\/?*[\]]/g, " ").slice(0, 31).trim() || "Sheet1";
|
|
22
|
+
const workbook = (name) => `${xml}<workbook xmlns="${mainNs}" xmlns:r="${relNs}"><sheets><sheet name="${escapeXml(sheetName(name))}" sheetId="1" r:id="rId1"/></sheets></workbook>`;
|
|
23
|
+
const styles = (formats) => {
|
|
24
|
+
const numFmts = formats.map((code, index) => `<numFmt numFmtId="${164 + index}" formatCode="${escapeXml(code)}"/>`).join("");
|
|
25
|
+
const xfs = formats.map((_, index) => `<xf numFmtId="${164 + index}" fontId="0" fillId="0" borderId="0" xfId="0" applyNumberFormat="1"/>`).join("");
|
|
26
|
+
return `${xml}<styleSheet xmlns="${mainNs}">${formats.length > 0 ? `<numFmts count="${formats.length}">${numFmts}</numFmts>` : ""}<fonts count="2"><font><sz val="11"/><name val="Calibri"/></font><font><b/><sz val="11"/><name val="Calibri"/></font></fonts><fills count="2"><fill><patternFill patternType="none"/></fill><fill><patternFill patternType="gray125"/></fill></fills><borders count="1"><border><left/><right/><top/><bottom/><diagonal/></border></borders><cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs><cellXfs count="${2 + formats.length}"><xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/><xf numFmtId="0" fontId="1" fillId="0" borderId="0" xfId="0" applyFont="1"/>${xfs}</cellXfs><cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles></styleSheet>`;
|
|
27
|
+
};
|
|
28
|
+
const encoder = new TextEncoder();
|
|
29
|
+
/**
|
|
30
|
+
* Writes a one-sheet .xlsx file as it goes, handing each compressed piece to `onChunk`: a bold, frozen header row, then
|
|
31
|
+
* `rows` as often as they come, and `finish` once. Text is written inline in its cell rather than in a table of shared
|
|
32
|
+
* strings, so the writer holds no more than the rows it is given.
|
|
33
|
+
*/
|
|
34
|
+
const createXlsxWriter = (sheet, onChunk) => {
|
|
35
|
+
const zip = new Zip((error, chunk, final) => {
|
|
36
|
+
if (error) throw error;
|
|
37
|
+
onChunk(chunk, final);
|
|
38
|
+
});
|
|
39
|
+
const add = (path, text) => {
|
|
40
|
+
const file = new ZipDeflate(path, { level: 6 });
|
|
41
|
+
zip.add(file);
|
|
42
|
+
file.push(encoder.encode(text), true);
|
|
43
|
+
};
|
|
44
|
+
add("[Content_Types].xml", contentTypes);
|
|
45
|
+
add("_rels/.rels", rootRels);
|
|
46
|
+
add("xl/workbook.xml", workbook(sheet.name));
|
|
47
|
+
add("xl/_rels/workbook.xml.rels", workbookRels);
|
|
48
|
+
const worksheet = new ZipDeflate("xl/worksheets/sheet1.xml", { level: 6 });
|
|
49
|
+
zip.add(worksheet);
|
|
50
|
+
const cols = sheet.widths.map((width, index) => `<col min="${index + 1}" max="${index + 1}" width="${width}" customWidth="1"/>`).join("");
|
|
51
|
+
const frozen = "<sheetViews><sheetView workbookViewId=\"0\"><pane ySplit=\"1\" topLeftCell=\"A2\" activePane=\"bottomLeft\" state=\"frozen\"/></sheetView></sheetViews>";
|
|
52
|
+
const header = `<row r="1">${sheet.headers.map((text, index) => textCell(`${columnLetters(index)}1`, text, " s=\"1\"")).join("")}</row>`;
|
|
53
|
+
worksheet.push(encoder.encode(`${xml}<worksheet xmlns="${mainNs}">${frozen}${cols ? `<cols>${cols}</cols>` : ""}<sheetData>${header}`));
|
|
54
|
+
const letters = sheet.headers.map((_, index) => columnLetters(index));
|
|
55
|
+
const formats = [];
|
|
56
|
+
const styleOf = (format) => {
|
|
57
|
+
const index = formats.indexOf(format);
|
|
58
|
+
if (index >= 0) return index + 2;
|
|
59
|
+
formats.push(format);
|
|
60
|
+
return formats.length + 1;
|
|
61
|
+
};
|
|
62
|
+
const cellXml = (cell, ref) => {
|
|
63
|
+
switch (cell.kind) {
|
|
64
|
+
case "empty": return "";
|
|
65
|
+
case "text": return textCell(ref, cell.value);
|
|
66
|
+
case "boolean": return `<c r="${ref}" t="b"><v>${cell.value ? 1 : 0}</v></c>`;
|
|
67
|
+
case "number": return `<c r="${ref}"${cell.format ? ` s="${styleOf(cell.format)}"` : ""}><v>${cell.value}</v></c>`;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
let rowNumber = 1;
|
|
71
|
+
const rows = (cells) => {
|
|
72
|
+
let text = "";
|
|
73
|
+
for (const row of cells) {
|
|
74
|
+
rowNumber += 1;
|
|
75
|
+
if (rowNumber > 1048576) throw new Error(`A sheet holds at most ${xlsxMaxRows} rows`);
|
|
76
|
+
text += `<row r="${rowNumber}">${row.map((cell, index) => cellXml(cell, `${letters[index]}${rowNumber}`)).join("")}</row>`;
|
|
77
|
+
}
|
|
78
|
+
worksheet.push(encoder.encode(text));
|
|
79
|
+
};
|
|
80
|
+
const finish = () => {
|
|
81
|
+
worksheet.push(encoder.encode("</sheetData></worksheet>"), true);
|
|
82
|
+
add("xl/styles.xml", styles(formats));
|
|
83
|
+
zip.end();
|
|
84
|
+
};
|
|
85
|
+
return {
|
|
86
|
+
rows,
|
|
87
|
+
finish
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
//#endregion
|
|
91
|
+
export { columnLetters, createXlsxWriter, xlsxMaxRows };
|
package/dist/app/list-page.js
CHANGED
|
@@ -29,6 +29,8 @@ import { Sheet } from "../primitives/sheet.js";
|
|
|
29
29
|
import { useDeleteFlow } from "./delete/use-delete-flow.js";
|
|
30
30
|
import { useRestore } from "./delete/use-restore.js";
|
|
31
31
|
import { ImportButton } from "./import/import-button.js";
|
|
32
|
+
import { exportOffered, exportsSelection } from "./export/export-options.js";
|
|
33
|
+
import { useExportFlow } from "./export/use-export-flow.js";
|
|
32
34
|
import { SignInPrompt } from "../auth/sign-in-prompt.js";
|
|
33
35
|
import { ListCards } from "./displays/list-cards.js";
|
|
34
36
|
import { MapDisplay } from "./displays/map-display.js";
|
|
@@ -44,7 +46,7 @@ import { facetNames, filterChips } from "./searches/saved-search.js";
|
|
|
44
46
|
import { SaveSearchButton } from "./searches/save-search-button.js";
|
|
45
47
|
import { SavedSearches } from "./searches/saved-searches.js";
|
|
46
48
|
import { useMemo, useState } from "react";
|
|
47
|
-
import { Plus, SlidersHorizontal } from "lucide-react";
|
|
49
|
+
import { Download, Plus, SlidersHorizontal } from "lucide-react";
|
|
48
50
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
49
51
|
import { createWhere } from "@protobase/schema";
|
|
50
52
|
import { tableDisplay } from "@protobase/schema/internal";
|
|
@@ -272,6 +274,23 @@ const LoadedList = ({ model, address, scope, heading }) => {
|
|
|
272
274
|
chips: filterChips(model, view, filterText, facetNames(config))
|
|
273
275
|
});
|
|
274
276
|
const importButton = !scope && /* @__PURE__ */ jsx(ImportButton, { model });
|
|
277
|
+
const exportFlow = useExportFlow({
|
|
278
|
+
model,
|
|
279
|
+
view,
|
|
280
|
+
resources: meta.resources,
|
|
281
|
+
views: meta.views,
|
|
282
|
+
plural,
|
|
283
|
+
filter: loadFilter,
|
|
284
|
+
orderBy
|
|
285
|
+
});
|
|
286
|
+
const exportButton = exportOffered(view) && /* @__PURE__ */ jsxs(Button, {
|
|
287
|
+
onClick: () => exportFlow.open(),
|
|
288
|
+
...!desktop && {
|
|
289
|
+
"aria-label": "Export",
|
|
290
|
+
title: "Export"
|
|
291
|
+
},
|
|
292
|
+
children: [/* @__PURE__ */ jsx(Download, { className: "size-3.5" }), desktop && "Export"]
|
|
293
|
+
});
|
|
275
294
|
const displaySwitch = (menu) => /* @__PURE__ */ jsx(DisplaySwitch, {
|
|
276
295
|
view,
|
|
277
296
|
value: display?.name ?? tableDisplay,
|
|
@@ -300,11 +319,14 @@ const LoadedList = ({ model, address, scope, heading }) => {
|
|
|
300
319
|
available: (row) => rowAllows(row, "delete"),
|
|
301
320
|
onSelect: (row) => void remove.start(target(row))
|
|
302
321
|
}] : []],
|
|
303
|
-
bulkActions:
|
|
322
|
+
bulkActions: [...exportOffered(view) && exportsSelection(model) ? [{
|
|
323
|
+
label: (count) => `Export ${count}`,
|
|
324
|
+
onRun: (rows) => exportFlow.open(rows)
|
|
325
|
+
}] : [], ...can(permissions, "delete") ? [{
|
|
304
326
|
label: (count) => `Delete ${count}`,
|
|
305
327
|
destructive: true,
|
|
306
328
|
onRun: (rows) => remove.startBulk(rows.map(target))
|
|
307
|
-
}] : [],
|
|
329
|
+
}] : []],
|
|
308
330
|
rowNumbers: false,
|
|
309
331
|
pinKey: auth.kind === "signed-in" ? pinnedStorageKey(auth.user.id, resource) : void 0,
|
|
310
332
|
scroll: "parent",
|
|
@@ -344,6 +366,7 @@ const LoadedList = ({ model, address, scope, heading }) => {
|
|
|
344
366
|
className: "flex min-h-0 flex-1 flex-col gap-3 px-5 py-4",
|
|
345
367
|
children: [
|
|
346
368
|
remove.dialogs,
|
|
369
|
+
exportFlow.dialogs,
|
|
347
370
|
/* @__PURE__ */ jsx(PageHeader, {
|
|
348
371
|
title: heading?.title ?? plural,
|
|
349
372
|
subtitle: heading ? heading.subtitle : view?.help,
|
|
@@ -352,10 +375,12 @@ const LoadedList = ({ model, address, scope, heading }) => {
|
|
|
352
375
|
displaySwitch(false),
|
|
353
376
|
saveSearch,
|
|
354
377
|
importButton,
|
|
378
|
+
exportButton,
|
|
355
379
|
createButton
|
|
356
380
|
] }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
357
381
|
saveSearch,
|
|
358
382
|
importButton,
|
|
383
|
+
exportButton,
|
|
359
384
|
createButton
|
|
360
385
|
] })
|
|
361
386
|
}),
|
package/dist/data/query-keys.js
CHANGED
|
@@ -90,6 +90,13 @@ const keys = {
|
|
|
90
90
|
resource,
|
|
91
91
|
filter
|
|
92
92
|
],
|
|
93
|
+
/** How many rows an export of the list would hold, exactly up to 10,000; starts with `list`, so it refreshes with the list. */
|
|
94
|
+
exportCount: (resource, filter) => [
|
|
95
|
+
"list",
|
|
96
|
+
resource,
|
|
97
|
+
filter,
|
|
98
|
+
"export-count"
|
|
99
|
+
],
|
|
93
100
|
firstRecord: (resource, filter, orderBy) => [
|
|
94
101
|
"first-record",
|
|
95
102
|
resource,
|
package/dist/package.js
CHANGED