@mshuja288/aksuicore 0.4.1 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/aksui.css +75 -1
- package/dist/index.cjs +133 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +20 -2
- package/dist/index.d.ts +20 -2
- package/dist/index.js +133 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -2
- package/src/patterns/RecordWorkspace.tsx +157 -1
- package/src/patterns/index.ts +3 -0
- package/src/theme/patterns.css +75 -1
package/package.json
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mshuja288/aksuicore",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "Shared UI components and templates for Ikssam applications",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
|
-
"private": false,
|
|
7
6
|
"type": "module",
|
|
8
7
|
"main": "./dist/index.cjs",
|
|
9
8
|
"module": "./dist/index.js",
|
|
@@ -65,12 +65,34 @@ export type ExportColumnRef = {
|
|
|
65
65
|
header: string;
|
|
66
66
|
};
|
|
67
67
|
|
|
68
|
+
export type BulkImportMode = "insert" | "update" | "upsert";
|
|
69
|
+
|
|
70
|
+
export type BulkImportError = {
|
|
71
|
+
row: number;
|
|
72
|
+
field?: string | null;
|
|
73
|
+
code: string;
|
|
74
|
+
message: string;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export type BulkImportResult = {
|
|
78
|
+
totalRows: number;
|
|
79
|
+
failed: number;
|
|
80
|
+
applied: boolean;
|
|
81
|
+
errors: BulkImportError[];
|
|
82
|
+
};
|
|
83
|
+
|
|
68
84
|
const EXPORT_FORMAT_OPTIONS: { format: ExportDownloadFormat; label: string }[] = [
|
|
69
85
|
{ format: "csv", label: "CSV" },
|
|
70
86
|
{ format: "xlsx", label: "Excel" },
|
|
71
87
|
{ format: "json", label: "JSON" },
|
|
72
88
|
];
|
|
73
89
|
|
|
90
|
+
const BULK_IMPORT_MODE_OPTIONS: { mode: BulkImportMode; label: string }[] = [
|
|
91
|
+
{ mode: "insert", label: "Insert" },
|
|
92
|
+
{ mode: "update", label: "Update" },
|
|
93
|
+
{ mode: "upsert", label: "Upsert" },
|
|
94
|
+
];
|
|
95
|
+
|
|
74
96
|
type SortDirection = "asc" | "desc";
|
|
75
97
|
type SortState = { key: string; direction: SortDirection };
|
|
76
98
|
type DetailLine = { label: string; value: string };
|
|
@@ -194,6 +216,16 @@ function PreviewIcon() {
|
|
|
194
216
|
);
|
|
195
217
|
}
|
|
196
218
|
|
|
219
|
+
function UploadIcon() {
|
|
220
|
+
return (
|
|
221
|
+
<svg viewBox="0 0 20 20" aria-hidden="true">
|
|
222
|
+
<path d="M10 16.5V7.3" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
|
|
223
|
+
<path d="M6.5 10.8 10 7.2l3.5 3.6" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" />
|
|
224
|
+
<path d="M4 4.5h12" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
|
|
225
|
+
</svg>
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
197
229
|
function DownloadIcon() {
|
|
198
230
|
return (
|
|
199
231
|
<svg viewBox="0 0 20 20" aria-hidden="true">
|
|
@@ -258,7 +290,11 @@ export function RecordWorkspace<T>({
|
|
|
258
290
|
canDelete,
|
|
259
291
|
onDownload,
|
|
260
292
|
canDownload = true,
|
|
293
|
+
canExport,
|
|
261
294
|
downloadBusy = false,
|
|
295
|
+
onBulkImport,
|
|
296
|
+
canImport = true,
|
|
297
|
+
importBusy = false,
|
|
262
298
|
showColumnPicker = true,
|
|
263
299
|
columnPrefsKey,
|
|
264
300
|
visibleColumnKeys,
|
|
@@ -285,8 +321,13 @@ export function RecordWorkspace<T>({
|
|
|
285
321
|
* Second argument lists currently visible columns (exportKey ?? key).
|
|
286
322
|
*/
|
|
287
323
|
onDownload?: (format: ExportDownloadFormat, columns: ExportColumnRef[]) => void;
|
|
324
|
+
/** Alias for `canDownload` (RBAC). When set, export requires this and `canDownload` to be true. */
|
|
325
|
+
canExport?: boolean;
|
|
288
326
|
canDownload?: boolean;
|
|
289
327
|
downloadBusy?: boolean;
|
|
328
|
+
onBulkImport?: (mode: BulkImportMode, file: File) => Promise<BulkImportResult>;
|
|
329
|
+
canImport?: boolean;
|
|
330
|
+
importBusy?: boolean;
|
|
290
331
|
/** Column checklist in the side rail. Default true. */
|
|
291
332
|
showColumnPicker?: boolean;
|
|
292
333
|
/** Persist visible column keys in localStorage under this key. */
|
|
@@ -314,6 +355,11 @@ export function RecordWorkspace<T>({
|
|
|
314
355
|
const [sideOpen, setSideOpen] = useState(false);
|
|
315
356
|
const [toolsDrawerOpen, setToolsDrawerOpen] = useState(false);
|
|
316
357
|
const [downloadOpen, setDownloadOpen] = useState(false);
|
|
358
|
+
const [bulkOpen, setBulkOpen] = useState(false);
|
|
359
|
+
const [bulkMode, setBulkMode] = useState<BulkImportMode>("upsert");
|
|
360
|
+
const [bulkFile, setBulkFile] = useState<File | null>(null);
|
|
361
|
+
const [bulkReport, setBulkReport] = useState<BulkImportResult | null>(null);
|
|
362
|
+
const bulkFileInputRef = useRef<HTMLInputElement | null>(null);
|
|
317
363
|
const [columnsOpen, setColumnsOpen] = useState(false);
|
|
318
364
|
const [previewEnabled, setPreviewEnabled] = useState(false);
|
|
319
365
|
const [previewKey, setPreviewKey] = useState<string | number | null>(null);
|
|
@@ -425,7 +471,9 @@ export function RecordWorkspace<T>({
|
|
|
425
471
|
const start = showAll ? 0 : (currentPage - 1) * numericPageSize;
|
|
426
472
|
const pageItems = showAll ? sorted : sorted.slice(start, start + numericPageSize);
|
|
427
473
|
const showAdd = canAdd && Boolean(onAdd);
|
|
428
|
-
const
|
|
474
|
+
const exportAllowed = (canExport ?? true) && canDownload;
|
|
475
|
+
const showDownload = Boolean(onDownload) && exportAllowed;
|
|
476
|
+
const showBulkImport = Boolean(onBulkImport) && canImport;
|
|
429
477
|
const showFilters = filterableColumns.length > 0;
|
|
430
478
|
const showColumns = showColumnPicker && columns.length > 0;
|
|
431
479
|
const previewItem = previewKey === null ? null : sorted.find((item) => rowKey(item) === previewKey) ?? null;
|
|
@@ -478,6 +526,30 @@ export function RecordWorkspace<T>({
|
|
|
478
526
|
setDownloadOpen(false);
|
|
479
527
|
}
|
|
480
528
|
|
|
529
|
+
async function submitBulkImport() {
|
|
530
|
+
if (!onBulkImport || importBusy || !bulkFile) {
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
setBulkReport(null);
|
|
534
|
+
try {
|
|
535
|
+
const report = await onBulkImport(bulkMode, bulkFile);
|
|
536
|
+
setBulkReport(report);
|
|
537
|
+
if (report.applied) {
|
|
538
|
+
setBulkFile(null);
|
|
539
|
+
if (bulkFileInputRef.current) {
|
|
540
|
+
bulkFileInputRef.current.value = "";
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
} catch {
|
|
544
|
+
setBulkReport({
|
|
545
|
+
totalRows: 0,
|
|
546
|
+
failed: 1,
|
|
547
|
+
applied: false,
|
|
548
|
+
errors: [{ row: 0, code: "ImportFailed", message: "Import request failed." }],
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
481
553
|
function cellText(column: TableColumn<T>, item: T) {
|
|
482
554
|
const raw = column.display ? column.display(item) : column.value(item);
|
|
483
555
|
return raw === null || raw === undefined || raw === "" ? "—" : String(raw);
|
|
@@ -614,6 +686,21 @@ export function RecordWorkspace<T>({
|
|
|
614
686
|
<DownloadIcon />
|
|
615
687
|
</button>
|
|
616
688
|
) : null}
|
|
689
|
+
{showBulkImport ? (
|
|
690
|
+
<button
|
|
691
|
+
type="button"
|
|
692
|
+
className="ghost side-rail-icon"
|
|
693
|
+
title="Bulk entry"
|
|
694
|
+
aria-label="Bulk entry"
|
|
695
|
+
disabled={importBusy}
|
|
696
|
+
onClick={() => {
|
|
697
|
+
setSideOpen(true);
|
|
698
|
+
setBulkOpen(true);
|
|
699
|
+
}}
|
|
700
|
+
>
|
|
701
|
+
<UploadIcon />
|
|
702
|
+
</button>
|
|
703
|
+
) : null}
|
|
617
704
|
{showFilters ? (
|
|
618
705
|
<button
|
|
619
706
|
type="button"
|
|
@@ -742,6 +829,75 @@ export function RecordWorkspace<T>({
|
|
|
742
829
|
</div>
|
|
743
830
|
) : null}
|
|
744
831
|
|
|
832
|
+
{showBulkImport ? (
|
|
833
|
+
<div className="side-rail-bulk">
|
|
834
|
+
<button
|
|
835
|
+
type="button"
|
|
836
|
+
className="ghost side-rail-bulk-toggle"
|
|
837
|
+
aria-expanded={bulkOpen}
|
|
838
|
+
disabled={importBusy}
|
|
839
|
+
onClick={() => setBulkOpen((open) => !open)}
|
|
840
|
+
>
|
|
841
|
+
<UploadIcon />
|
|
842
|
+
<span>{importBusy ? "Importing…" : "Bulk entry"}</span>
|
|
843
|
+
</button>
|
|
844
|
+
{bulkOpen ? (
|
|
845
|
+
<div className="side-rail-bulk-panel" role="group" aria-label="Bulk entry">
|
|
846
|
+
<label className="side-rail-bulk-mode">
|
|
847
|
+
Mode
|
|
848
|
+
<Select
|
|
849
|
+
value={bulkMode}
|
|
850
|
+
options={BULK_IMPORT_MODE_OPTIONS.map((option) => ({ value: option.mode, label: option.label }))}
|
|
851
|
+
onChange={(value) => setBulkMode(value as BulkImportMode)}
|
|
852
|
+
/>
|
|
853
|
+
</label>
|
|
854
|
+
<label className="side-rail-bulk-file">
|
|
855
|
+
File
|
|
856
|
+
<input
|
|
857
|
+
ref={bulkFileInputRef}
|
|
858
|
+
type="file"
|
|
859
|
+
accept=".csv,.xlsx,.json"
|
|
860
|
+
disabled={importBusy}
|
|
861
|
+
onChange={(event) => {
|
|
862
|
+
setBulkFile(event.target.files?.[0] ?? null);
|
|
863
|
+
setBulkReport(null);
|
|
864
|
+
}}
|
|
865
|
+
/>
|
|
866
|
+
</label>
|
|
867
|
+
<button
|
|
868
|
+
type="button"
|
|
869
|
+
className="side-rail-bulk-submit"
|
|
870
|
+
disabled={importBusy || !bulkFile}
|
|
871
|
+
onClick={() => void submitBulkImport()}
|
|
872
|
+
>
|
|
873
|
+
{importBusy ? "Importing…" : "Import"}
|
|
874
|
+
</button>
|
|
875
|
+
{bulkReport ? (
|
|
876
|
+
<div className="side-rail-bulk-report" role="status">
|
|
877
|
+
<p>
|
|
878
|
+
{bulkReport.applied
|
|
879
|
+
? `Applied ${bulkReport.totalRows} row(s).`
|
|
880
|
+
: `${bulkReport.failed} error(s) in ${bulkReport.totalRows} row(s).`}
|
|
881
|
+
</p>
|
|
882
|
+
{bulkReport.errors.length > 0 ? (
|
|
883
|
+
<ul className="side-rail-bulk-errors">
|
|
884
|
+
{bulkReport.errors.slice(0, 8).map((error, index) => (
|
|
885
|
+
<li key={`${error.row}-${error.code}-${index}`}>
|
|
886
|
+
Row {error.row}: {error.message}
|
|
887
|
+
</li>
|
|
888
|
+
))}
|
|
889
|
+
{bulkReport.errors.length > 8 ? (
|
|
890
|
+
<li>…and {bulkReport.errors.length - 8} more</li>
|
|
891
|
+
) : null}
|
|
892
|
+
</ul>
|
|
893
|
+
) : null}
|
|
894
|
+
</div>
|
|
895
|
+
) : null}
|
|
896
|
+
</div>
|
|
897
|
+
) : null}
|
|
898
|
+
</div>
|
|
899
|
+
) : null}
|
|
900
|
+
|
|
745
901
|
{showFilters ? (
|
|
746
902
|
<div className="side-rail-filters">
|
|
747
903
|
<div className="filter-panel-head">
|
package/src/patterns/index.ts
CHANGED
package/src/theme/patterns.css
CHANGED
|
@@ -405,7 +405,8 @@ button.side-rail-add:hover {
|
|
|
405
405
|
}
|
|
406
406
|
|
|
407
407
|
.side-rail-columns,
|
|
408
|
-
.side-rail-download
|
|
408
|
+
.side-rail-download,
|
|
409
|
+
.side-rail-bulk {
|
|
409
410
|
display: grid;
|
|
410
411
|
gap: 8px;
|
|
411
412
|
}
|
|
@@ -533,6 +534,79 @@ button.side-rail-download-format:disabled {
|
|
|
533
534
|
cursor: default;
|
|
534
535
|
}
|
|
535
536
|
|
|
537
|
+
button.side-rail-bulk-toggle {
|
|
538
|
+
display: inline-flex;
|
|
539
|
+
align-items: center;
|
|
540
|
+
justify-content: center;
|
|
541
|
+
gap: 8px;
|
|
542
|
+
width: 100%;
|
|
543
|
+
padding: 10px 12px;
|
|
544
|
+
border: 1px solid var(--aks-border, #d7dde5);
|
|
545
|
+
border-radius: var(--aks-radius);
|
|
546
|
+
background: #fff;
|
|
547
|
+
color: var(--aks-brand);
|
|
548
|
+
box-shadow: none;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
button.side-rail-bulk-toggle svg {
|
|
552
|
+
width: 18px;
|
|
553
|
+
height: 18px;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
button.side-rail-bulk-toggle:hover:not(:disabled) {
|
|
557
|
+
background: #f5f7fa;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
button.side-rail-bulk-toggle:disabled {
|
|
561
|
+
opacity: 0.55;
|
|
562
|
+
cursor: default;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
.side-rail-bulk-panel {
|
|
566
|
+
display: grid;
|
|
567
|
+
gap: 10px;
|
|
568
|
+
padding: 8px;
|
|
569
|
+
border: 1px solid var(--aks-line);
|
|
570
|
+
border-radius: var(--aks-radius);
|
|
571
|
+
background: #faf9f7;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
label.side-rail-bulk-mode,
|
|
575
|
+
label.side-rail-bulk-file {
|
|
576
|
+
display: grid;
|
|
577
|
+
gap: 6px;
|
|
578
|
+
margin: 0;
|
|
579
|
+
font-size: 13px;
|
|
580
|
+
font-weight: 600;
|
|
581
|
+
color: var(--aks-brand);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
button.side-rail-bulk-submit {
|
|
585
|
+
width: 100%;
|
|
586
|
+
padding: 10px 12px;
|
|
587
|
+
border: none;
|
|
588
|
+
border-radius: var(--aks-radius);
|
|
589
|
+
background: var(--aks-accent, #c45c26);
|
|
590
|
+
color: #fff;
|
|
591
|
+
font-weight: 600;
|
|
592
|
+
box-shadow: none;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
button.side-rail-bulk-submit:disabled {
|
|
596
|
+
opacity: 0.5;
|
|
597
|
+
cursor: default;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
.side-rail-bulk-report {
|
|
601
|
+
font-size: 12px;
|
|
602
|
+
color: var(--aks-brand);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
.side-rail-bulk-errors {
|
|
606
|
+
margin: 6px 0 0;
|
|
607
|
+
padding-left: 18px;
|
|
608
|
+
}
|
|
609
|
+
|
|
536
610
|
.side-rail-filters {
|
|
537
611
|
display: grid;
|
|
538
612
|
gap: 12px;
|