@xeplr/ui-utils 1.0.9 → 1.0.10
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/README.md
CHANGED
|
@@ -118,7 +118,7 @@ export default function SelectCompany() {
|
|
|
118
118
|
| `useNumberFieldController(props)` | NumberFieldPage props | `type, raw, numeric, error, placeholder, disabled, decimals, formatDisplay, affix {list, selected, position, labelKey, valueKey} \| null, handleNumberChange, handleAffixChange, handleBlur` |
|
|
119
119
|
| `useTextFieldController(props)` | TextFieldPage props | `type, raw, error, remaining, placeholder, disabled, required, maxLength, rows, label, help, handleChange, handleBlur, handleKeyDown, handleClear` |
|
|
120
120
|
| `useFileUploadController(options)` | FileUploadPage props | `files, uploading, progress, error, result, dragOver, inputRef, handleChange, handleDragOver, handleDragLeave, handleDrop, handleUpload, openFilePicker, removeFile(i), clear, allowedTypes, maxSize, maxFiles` |
|
|
121
|
-
| `useDataImportController(options)` | DataImportPage props | `file, inspecting, inspected, mappings, committing, results, error, handleFileChange, updateMapping(i, patch), handleCommit, reset, accept, targetLabel` |
|
|
121
|
+
| `useDataImportController(options)` | DataImportPage props | `file, inspecting, inspected, mappings, committing, results, error, handleFileChange, updateMapping(i, patch), setFix(i, key, on), checksOf(part), handleCommit, reset, accept, targetLabel` |
|
|
122
122
|
| `useImportHistoryController(options)` | ImportHistoryPage props | `jobs, loading, error, rollingBackId, deletingId, reload, handleRollback(job), handleDelete(job)` |
|
|
123
123
|
| `useGridDisplayerController(props)` | GridDisplayerPage props | `query, setQuery, sortFields, sortValue, setSortValue, displayRows, tileFormatComponent, onSelect, onAddNew` |
|
|
124
124
|
| `useColorPickerController(props)` | ColorPickerPage props | `label, value, hex, opacity, swatches [{color, selected}], custom, allowNone, allowOpacity, previewText, pick(hex), setOpacity(0–100), clear` |
|
|
@@ -146,7 +146,7 @@ export default function SelectCompany() {
|
|
|
146
146
|
| `toggleOpen`, `initialOpen` | `(open, key, multiple?) → keys`, `(items, initial?, multiple?) → keys` | Which expanders are open |
|
|
147
147
|
| `toggleBranch`, `decorateTree` | `(open, parentKey, key) → open`, `(nodes, open) → nodes` | Which branch of a settings tree is open at each level; `open` is `{ [parentKey]: key }`, the root's parent `''` |
|
|
148
148
|
| `filterGridRows`, `sortGridRows` | `(rows, query, fields)`, `(rows, field, 'asc'\|'desc')` | Grid search and sort. An array field matches if any entry matches. |
|
|
149
|
-
| `sanitizeTargetName`, `buildInitialMappings` | `(name) → string`, `({ parts?, originalName? }) → [{ part, target, include }]` | Data import mapping |
|
|
149
|
+
| `sanitizeTargetName`, `buildInitialMappings`, `checksFor` | `(name) → string`, `({ parts?, originalName?, checks? }) → [{ part, target, include, fixes }]`, `(inspectResult, part) → checks[]` | Data import mapping |
|
|
150
150
|
| `configureUpload`, `uploadFile` | `({ url, headers, getHeaders })`, `(files, { url, fieldName, extraData, onProgress }) → Promise` | Multipart upload over XHR |
|
|
151
151
|
| `attachProgressSource`, `progressSourceAttached`, `subscribeToProgress`, `describeProgress`, `describeComplete` | `(subscribe)`, `() → boolean`, `(key, handler) → unsubscribe`, `(data, noun?)`, `(data, noun?)` | The progress event source and its sentences |
|
|
152
152
|
|
|
@@ -294,13 +294,15 @@ configureUpload({ url: '/uploads', getHeaders: () => ({ Authorization: 'Bearer '
|
|
|
294
294
|
|
|
295
295
|
| prop | type | meaning |
|
|
296
296
|
|---|---|---|
|
|
297
|
-
| `onInspect` | `(filePath, file) => Promise<{ format, parts? }>` | **Required.** Say what an uploaded file contains. `parts` holds, for example, an Excel workbook's sheet names. |
|
|
298
|
-
| `onCommit` | `(filePath, [{ part, target }], { originalName }) => Promise<{ results } \| results[]>` | **Required.** Load the included parts into their targets. |
|
|
297
|
+
| `onInspect` | `(filePath, file) => Promise<{ format, parts?, checks? }>` | **Required.** Say what an uploaded file contains. `parts` holds, for example, an Excel workbook's sheet names. `checks` says what is wrong with a part before it is loaded — see below. |
|
|
298
|
+
| `onCommit` | `(filePath, [{ part, target, fixes }], { originalName }) => Promise<{ results } \| results[]>` | **Required.** Load the included parts into their targets. `fixes` is the person's choice for each fix the checks offered. |
|
|
299
299
|
| `accept` | `string` | The file input's `accept` attribute, for example `'.csv,.json,.xlsx,.xls'` |
|
|
300
300
|
| `targetLabel` | `string` | Placeholder for the target name (default `'Target'`) |
|
|
301
301
|
|
|
302
302
|
The file is sent through this package's own `uploadFile()` to the URL set by `configureUpload`, not through a prop. That keeps the component free of app-specific upload wiring. The apps point it at `@xeplr/base-apis`' generic upload route. The upload response must include `filePath`. With `parts`, each part becomes a row (included by default) whose target is `sanitizeTargetName(part)`. Without `parts`, there is one row named after the file. Result rows show `target (part)` and then `rows` + " rows", "Done", or `error`, depending on `success`.
|
|
303
303
|
|
|
304
|
+
**Checks, before anything is loaded.** `onInspect` may return `checks: { [part]: [{ key, message, fixLabel?, fixDefault?, examples? }] }` (the key `''` for a one-unit file). Each shows under its part as a warning with its message and examples. One with a `fixLabel` offers the fix as a checkbox, on unless `fixDefault: false`; the choice reaches `onCommit` as `fixes[key]`. A result row's `note` is shown under it — what a fix did. The component knows no particular check: the app decides what to look for and what its fix means (BI: rows pasted into one cell, recovered).
|
|
305
|
+
|
|
304
306
|
`sanitizeTargetName('Sales 2024.xlsx')` returns `'sales_2024'`. It lowercases, drops the extension, turns runs of non-`[a-z0-9_]` characters into `_`, trims `_` from both ends, and falls back to `'table'`.
|
|
305
307
|
|
|
306
308
|
## ImportHistoryPage
|
package/package.json
CHANGED
|
@@ -16,8 +16,26 @@ export function sanitizeTargetName(name) {
|
|
|
16
16
|
.replace(/^_+|_+$/g, '') || 'table';
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
//
|
|
20
|
-
//
|
|
19
|
+
// What the inspect step found wrong with one part, if anything:
|
|
20
|
+
// inspectResult.checks = { [part]: [{ key, message, fixLabel?, fixDefault? }] }
|
|
21
|
+
// (the key '' for a file that is one unit). A check with a fixLabel offers a
|
|
22
|
+
// fix the person switches on or off before importing; its choice travels
|
|
23
|
+
// with the mapping as fixes[key].
|
|
24
|
+
export function checksFor(inspectResult, part) {
|
|
25
|
+
var all = (inspectResult && inspectResult.checks) || {};
|
|
26
|
+
return all[part == null ? '' : part] || [];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function initialFixes(inspectResult, part) {
|
|
30
|
+
var fixes = {};
|
|
31
|
+
checksFor(inspectResult, part).forEach(function(c) {
|
|
32
|
+
if (c.fixLabel) fixes[c.key] = c.fixDefault !== false;
|
|
33
|
+
});
|
|
34
|
+
return fixes;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// inspectResult: { filePath, format, originalName?, parts?: string[], checks? }
|
|
38
|
+
// -> [{ part: string|null, target: string, include: boolean, fixes: { [key]: boolean } }]
|
|
21
39
|
//
|
|
22
40
|
// parts present (e.g. Excel sheets) -> one row per part, all included by
|
|
23
41
|
// default. parts absent (e.g. CSV/JSON — the whole file is one unit) -> a
|
|
@@ -25,8 +43,8 @@ export function sanitizeTargetName(name) {
|
|
|
25
43
|
export function buildInitialMappings(inspectResult) {
|
|
26
44
|
if (inspectResult.parts && inspectResult.parts.length) {
|
|
27
45
|
return inspectResult.parts.map(function(part) {
|
|
28
|
-
return { part: part, target: sanitizeTargetName(part), include: true };
|
|
46
|
+
return { part: part, target: sanitizeTargetName(part), include: true, fixes: initialFixes(inspectResult, part) };
|
|
29
47
|
});
|
|
30
48
|
}
|
|
31
|
-
return [{ part: null, target: sanitizeTargetName(inspectResult.originalName || 'data'), include: true }];
|
|
49
|
+
return [{ part: null, target: sanitizeTargetName(inspectResult.originalName || 'data'), include: true, fixes: initialFixes(inspectResult, null) }];
|
|
32
50
|
}
|
|
@@ -2,7 +2,7 @@ import './dataImport.css';
|
|
|
2
2
|
|
|
3
3
|
export default function DataImportSample({
|
|
4
4
|
file, inspecting, inspected, mappings, committing, results, error,
|
|
5
|
-
handleFileChange, updateMapping, handleCommit, reset, accept, targetLabel
|
|
5
|
+
handleFileChange, updateMapping, setFix, checksOf, handleCommit, reset, accept, targetLabel
|
|
6
6
|
}) {
|
|
7
7
|
return (
|
|
8
8
|
<div className="xeplr-data-import">
|
|
@@ -37,6 +37,31 @@ export default function DataImportSample({
|
|
|
37
37
|
onChange={function(e) { updateMapping(i, { target: e.target.value }); }}
|
|
38
38
|
placeholder={targetLabel}
|
|
39
39
|
/>
|
|
40
|
+
{/* What the inspect step found, before anything is loaded —
|
|
41
|
+
and the fix, when one is offered, as a choice. */}
|
|
42
|
+
{(checksOf ? checksOf(m.part) : []).map(function(c) {
|
|
43
|
+
return (
|
|
44
|
+
<div key={c.key} className="xeplr-data-import-check" role="alert">
|
|
45
|
+
<p className="xeplr-data-import-check-message">{c.message}</p>
|
|
46
|
+
{c.fixLabel && (
|
|
47
|
+
<label className="xeplr-data-import-check-fix">
|
|
48
|
+
<input
|
|
49
|
+
type="checkbox"
|
|
50
|
+
checked={Boolean(m.fixes && m.fixes[c.key])}
|
|
51
|
+
disabled={!m.include}
|
|
52
|
+
onChange={function(e) { setFix(i, c.key, e.target.checked); }}
|
|
53
|
+
/>
|
|
54
|
+
{c.fixLabel}
|
|
55
|
+
</label>
|
|
56
|
+
)}
|
|
57
|
+
{(c.examples || []).length > 0 && (
|
|
58
|
+
<ul className="xeplr-data-import-check-examples">
|
|
59
|
+
{c.examples.map(function(x, xi) { return <li key={xi}>{x}</li>; })}
|
|
60
|
+
</ul>
|
|
61
|
+
)}
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
})}
|
|
40
65
|
</div>
|
|
41
66
|
);
|
|
42
67
|
})}
|
|
@@ -61,6 +86,7 @@ export default function DataImportSample({
|
|
|
61
86
|
<span className={r.success ? 'xeplr-data-import-result-ok' : 'xeplr-data-import-result-fail'}>
|
|
62
87
|
{r.success ? (r.rows != null ? r.rows + ' rows' : 'Done') : (r.error || 'Failed')}
|
|
63
88
|
</span>
|
|
89
|
+
{r.note && <span className="xeplr-data-import-result-note">{r.note}</span>}
|
|
64
90
|
</div>
|
|
65
91
|
);
|
|
66
92
|
})}
|
|
@@ -96,3 +96,22 @@
|
|
|
96
96
|
.xeplr-import-history-status-failed { color: var(--xeplr-danger, #ff6b6b); }
|
|
97
97
|
.xeplr-import-history-status-rolled-back { color: var(--xeplr-text-muted, #888); }
|
|
98
98
|
.xeplr-import-history-error { color: var(--xeplr-text-secondary, #999); }
|
|
99
|
+
|
|
100
|
+
/* What the inspect step found in a part — on its own line under the part,
|
|
101
|
+
with the fix (when one is offered) as a checkbox the person decides. */
|
|
102
|
+
.xeplr-data-import-mapping-row { flex-wrap: wrap; }
|
|
103
|
+
.xeplr-data-import-check {
|
|
104
|
+
flex-basis: 100%;
|
|
105
|
+
margin: 2px 0 4px;
|
|
106
|
+
padding: 10px 12px;
|
|
107
|
+
border-radius: 8px;
|
|
108
|
+
border: 1px solid var(--xeplr-warning, #a86a00);
|
|
109
|
+
background: var(--xeplr-warning-bg, rgba(168, 106, 0, 0.08));
|
|
110
|
+
color: var(--xeplr-text-primary, #e0e0e0);
|
|
111
|
+
font-size: 13px;
|
|
112
|
+
}
|
|
113
|
+
.xeplr-data-import-check-message { margin: 0 0 6px; line-height: 1.45; }
|
|
114
|
+
.xeplr-data-import-check-fix { display: flex; align-items: center; gap: 8px; font-weight: 600; cursor: pointer; }
|
|
115
|
+
.xeplr-data-import-check-examples { margin: 6px 0 0; padding-left: 18px; font-size: 12px; color: var(--xeplr-text-muted, #888); }
|
|
116
|
+
.xeplr-data-import-result-note { flex-basis: 100%; font-size: 12px; color: var(--xeplr-text-muted, #888); }
|
|
117
|
+
.xeplr-data-import-result-row { flex-wrap: wrap; }
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { useState } from 'react';
|
|
2
2
|
import { uploadFile } from '../fileUpload/uploadFile.js';
|
|
3
|
-
import { buildInitialMappings } from './dataImport.js';
|
|
3
|
+
import { buildInitialMappings, checksFor } from './dataImport.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @param {object} options
|
|
7
7
|
* @param {string} [options.accept] - file input accept attribute, e.g. '.csv,.json,.xlsx,.xls'
|
|
8
8
|
* @param {string} [options.targetLabel='Target'] - placeholder/label for the target-name field
|
|
9
|
-
* @param {(filePath: string, file: File) => Promise<{format: string, parts?: string[]}>} options.onInspect
|
|
9
|
+
* @param {(filePath: string, file: File) => Promise<{format: string, parts?: string[], checks?: object}>} options.onInspect
|
|
10
10
|
* - required. App-supplied: given a file ALREADY uploaded (see below), what
|
|
11
11
|
* does it contain? (format, and for a multi-part file like an Excel
|
|
12
12
|
* workbook, its part/sheet names).
|
|
13
|
-
* @param {(filePath: string, mappings: {part: string|null, target: string}[], meta: {originalName: string}) => Promise<{results: object[]}>} options.onCommit
|
|
13
|
+
* @param {(filePath: string, mappings: {part: string|null, target: string, fixes: object}[], meta: {originalName: string}) => Promise<{results: object[]}>} options.onCommit
|
|
14
|
+
* (each mapping's `fixes` is the person's choice for each fix the inspect
|
|
15
|
+
* step's `checks` offered — see checksFor in dataImport.js)
|
|
14
16
|
* - required. App-supplied: load the included parts into their targets.
|
|
15
17
|
* meta.originalName is the picked file's own name (not the server-side
|
|
16
18
|
* path) — pass it through if the app wants it recorded (e.g. import
|
|
@@ -61,6 +63,15 @@ export function useDataImportController(options = {}) {
|
|
|
61
63
|
}
|
|
62
64
|
}
|
|
63
65
|
|
|
66
|
+
// A fix the inspect step offered, switched on or off for one part.
|
|
67
|
+
function setFix(index, key, on) {
|
|
68
|
+
setMappings(function(prev) {
|
|
69
|
+
var next = prev.slice();
|
|
70
|
+
next[index] = Object.assign({}, next[index], { fixes: Object.assign({}, next[index].fixes, { [key]: on }) });
|
|
71
|
+
return next;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
64
75
|
function updateMapping(index, patch) {
|
|
65
76
|
setMappings(function(prev) {
|
|
66
77
|
var next = prev.slice();
|
|
@@ -75,7 +86,7 @@ export function useDataImportController(options = {}) {
|
|
|
75
86
|
setCommitting(true);
|
|
76
87
|
setError('');
|
|
77
88
|
try {
|
|
78
|
-
var payload = included.map(function(m) { return { part: m.part, target: m.target }; });
|
|
89
|
+
var payload = included.map(function(m) { return { part: m.part, target: m.target, fixes: m.fixes || {} }; });
|
|
79
90
|
var res = await options.onCommit(inspected.filePath, payload, { originalName: inspected.originalName });
|
|
80
91
|
setResults((res && res.results) || res || []);
|
|
81
92
|
} catch (err) {
|
|
@@ -87,7 +98,8 @@ export function useDataImportController(options = {}) {
|
|
|
87
98
|
|
|
88
99
|
return {
|
|
89
100
|
file, inspecting, inspected, mappings, committing, results, error,
|
|
90
|
-
handleFileChange, updateMapping, handleCommit, reset,
|
|
101
|
+
handleFileChange, updateMapping, setFix, handleCommit, reset,
|
|
102
|
+
checksOf: function(part) { return checksFor(inspected, part); },
|
|
91
103
|
accept: options.accept,
|
|
92
104
|
targetLabel: options.targetLabel || 'Target'
|
|
93
105
|
};
|