@campfire-interactive/volume-intelligence-ui 0.5.0 → 0.6.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/index.d.ts +38 -9
- package/dist/index.js +248 -403
- package/package.json +43 -43
package/dist/index.d.ts
CHANGED
|
@@ -124,7 +124,7 @@ interface TenantVolumeConfig {
|
|
|
124
124
|
sourceId: string;
|
|
125
125
|
source: SourceWithSegments;
|
|
126
126
|
}
|
|
127
|
-
type SegmentMappingCategory = 'hierarchy_level' | 'automotive_field' | 'period' | 'value';
|
|
127
|
+
type SegmentMappingCategory = 'hierarchy_level' | 'automotive_field' | 'period' | 'value' | 'dimension';
|
|
128
128
|
interface SegmentMappingRow {
|
|
129
129
|
id: string;
|
|
130
130
|
sourceId: string;
|
|
@@ -308,6 +308,19 @@ interface SourceAdminTransport {
|
|
|
308
308
|
listUploads(params?: UploadListParams): Promise<UploadListResponse>;
|
|
309
309
|
clearSourceData(sourceId: string, confirm: string): Promise<ClearSourceDataResult>;
|
|
310
310
|
getRequiredMappingStatus?(sourceId: string): Promise<RequiredMappingStatus>;
|
|
311
|
+
getMappableDimensions?(): Promise<MappableDimension[]>;
|
|
312
|
+
}
|
|
313
|
+
/** One dimension the operator can map a source column to. The category+targetKey
|
|
314
|
+
* say where the mapping is stored (a hierarchy level, an automotive field, or a
|
|
315
|
+
* dim slot); `label` is what the operator sees; `required` drives ordering +
|
|
316
|
+
* validation. The storage detail is deliberately hidden from the UI — the
|
|
317
|
+
* operator just picks a column per dimension. */
|
|
318
|
+
interface MappableDimension {
|
|
319
|
+
key: string;
|
|
320
|
+
label: string;
|
|
321
|
+
required: boolean;
|
|
322
|
+
category: 'hierarchy_level' | 'automotive_field' | 'dimension';
|
|
323
|
+
targetKey: string;
|
|
311
324
|
}
|
|
312
325
|
|
|
313
326
|
interface VolumeFilesTransport {
|
|
@@ -368,6 +381,22 @@ interface VolumeSourcesListProps {
|
|
|
368
381
|
*/
|
|
369
382
|
declare function VolumeSourcesList({ transport, onOpenSource, renderActiveSourceExtras, className }: VolumeSourcesListProps): react.JSX.Element;
|
|
370
383
|
|
|
384
|
+
/**
|
|
385
|
+
* Segment mapper — a catalog-driven, required-first assignment surface built for
|
|
386
|
+
* large column pools (100+). For each dimension the operator gives a source
|
|
387
|
+
* column; the storage detail (a hierarchy level / automotive field / dim slot)
|
|
388
|
+
* rides on the MappableDimension and is routed on save, never shown.
|
|
389
|
+
*
|
|
390
|
+
* Three ways to fill it, so a big pool stays manageable:
|
|
391
|
+
* 1. AUTO-MATCH on load — fuzzy-matches column names to the still-unmapped
|
|
392
|
+
* dimensions and pre-fills strong hits (tagged "auto", editable).
|
|
393
|
+
* 2. CLICK — click a dimension to focus it, then click a pool column.
|
|
394
|
+
* 3. DRAG — drag a pool column chip onto a dimension.
|
|
395
|
+
* The pool is searchable and hides already-used columns.
|
|
396
|
+
*
|
|
397
|
+
* When the host doesn't supply the tenant catalog, falls back to
|
|
398
|
+
* CANONICAL_MAPPABLE (level1..6 + required automotive) so the mapper still works.
|
|
399
|
+
*/
|
|
371
400
|
interface AutomotiveSlot {
|
|
372
401
|
key: string;
|
|
373
402
|
label: string;
|
|
@@ -377,11 +406,10 @@ interface AutomotiveSlot {
|
|
|
377
406
|
}
|
|
378
407
|
interface UnifiedSegmentMapperProps {
|
|
379
408
|
segments: VolumeSourceSegment[];
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
transformRuleLabels: Record<string, string>;
|
|
409
|
+
/** The tenant's mappable dimensions; falls back to CANONICAL_MAPPABLE. */
|
|
410
|
+
mappableDimensions?: MappableDimension[];
|
|
411
|
+
/** Existing mappings across all categories, used to prefill the pickers. */
|
|
412
|
+
initialMappings: SegmentMappingRow[];
|
|
385
413
|
onSave: (hierarchy: Array<{
|
|
386
414
|
targetKey: string;
|
|
387
415
|
segmentId: string;
|
|
@@ -389,12 +417,13 @@ interface UnifiedSegmentMapperProps {
|
|
|
389
417
|
targetKey: string;
|
|
390
418
|
segmentId: string;
|
|
391
419
|
transformRule?: string | null;
|
|
420
|
+
}>, dimensions: Array<{
|
|
421
|
+
targetKey: string;
|
|
422
|
+
segmentId: string;
|
|
392
423
|
}>) => Promise<void>;
|
|
393
424
|
saving: boolean;
|
|
394
|
-
hierarchyMode?: 'tree' | 'sparse';
|
|
395
|
-
baseHierarchyHint?: Partial<Record<number, string>>;
|
|
396
425
|
}
|
|
397
|
-
declare function UnifiedSegmentMapper({ segments,
|
|
426
|
+
declare function UnifiedSegmentMapper({ segments, mappableDimensions, initialMappings, onSave, saving, }: UnifiedSegmentMapperProps): react.JSX.Element;
|
|
398
427
|
|
|
399
428
|
interface VolumeSourceDetailProps {
|
|
400
429
|
transport: SourceAdminTransport;
|
package/dist/index.js
CHANGED
|
@@ -997,7 +997,7 @@ function VolumeSourcesList({ transport, onOpenSource, renderActiveSourceExtras,
|
|
|
997
997
|
|
|
998
998
|
// src/UnifiedSegmentMapper.tsx
|
|
999
999
|
import { useEffect as useEffect4, useMemo as useMemo3, useState as useState4 } from "react";
|
|
1000
|
-
import { Search,
|
|
1000
|
+
import { Search, Wand2 } from "lucide-react";
|
|
1001
1001
|
|
|
1002
1002
|
// src/constants.ts
|
|
1003
1003
|
var HIERARCHY_LEVEL_COUNT = 6;
|
|
@@ -1025,259 +1025,255 @@ var AUTOMOTIVE_TRANSFORM_LABELS = {
|
|
|
1025
1025
|
normalise_country: "Convert country code (e.g. USA \u2192 US)",
|
|
1026
1026
|
parse_iso_date: "Parse to ISO date (YYYY-MM-DD)"
|
|
1027
1027
|
};
|
|
1028
|
+
var CANONICAL_MAPPABLE = [
|
|
1029
|
+
{ key: "oem", label: "OEM", required: true, category: "hierarchy_level", targetKey: "level1" },
|
|
1030
|
+
{ key: "platform", label: "Platform", required: true, category: "hierarchy_level", targetKey: "level2" },
|
|
1031
|
+
{ key: "program", label: "Program", required: true, category: "hierarchy_level", targetKey: "level3" },
|
|
1032
|
+
{ key: "plant", label: "Plant", required: true, category: "hierarchy_level", targetKey: "level4" },
|
|
1033
|
+
{ key: "nameplate", label: "Nameplate", required: true, category: "hierarchy_level", targetKey: "level5" },
|
|
1034
|
+
{ key: "trim", label: "Trim Level", required: false, category: "hierarchy_level", targetKey: "level6" },
|
|
1035
|
+
{ key: "region", label: "Region", required: true, category: "automotive_field", targetKey: "region" },
|
|
1036
|
+
{ key: "plant_country", label: "Plant Country", required: true, category: "automotive_field", targetKey: "plant_country" },
|
|
1037
|
+
{ key: "sop_date", label: "SOP (Start of Production)", required: true, category: "automotive_field", targetKey: "sop_date" },
|
|
1038
|
+
{ key: "eop_date", label: "EOP (End of Production)", required: true, category: "automotive_field", targetKey: "eop_date" },
|
|
1039
|
+
{ key: "brand", label: "Brand", required: false, category: "automotive_field", targetKey: "brand" }
|
|
1040
|
+
];
|
|
1028
1041
|
|
|
1029
1042
|
// src/UnifiedSegmentMapper.tsx
|
|
1030
1043
|
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1031
|
-
var
|
|
1044
|
+
var autoTransform = (targetKey) => AUTOMOTIVE_SLOTS.find((s) => s.key === targetKey)?.defaultTransform ?? null;
|
|
1045
|
+
var norm = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
1046
|
+
var toks = (s) => s.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
|
|
1047
|
+
function score(dim, colLabel) {
|
|
1048
|
+
const c = norm(colLabel);
|
|
1049
|
+
if (!c) return 0;
|
|
1050
|
+
let best = 0;
|
|
1051
|
+
for (const name of [dim.label, dim.key, dim.targetKey]) {
|
|
1052
|
+
const d = norm(name);
|
|
1053
|
+
if (!d) continue;
|
|
1054
|
+
if (d === c) best = Math.max(best, 100);
|
|
1055
|
+
else if (c.includes(d) || d.includes(c)) best = Math.max(best, 75);
|
|
1056
|
+
}
|
|
1057
|
+
const ct = new Set(toks(colLabel));
|
|
1058
|
+
const dt = toks(dim.label);
|
|
1059
|
+
if (dt.length) {
|
|
1060
|
+
const overlap = dt.filter((t) => ct.has(t)).length / dt.length;
|
|
1061
|
+
best = Math.max(best, Math.round(overlap * 60));
|
|
1062
|
+
}
|
|
1063
|
+
return best;
|
|
1064
|
+
}
|
|
1065
|
+
var AUTO_THRESHOLD = 70;
|
|
1066
|
+
function autoMatch(dims, segments, existing) {
|
|
1067
|
+
const used = new Set(Object.values(existing).filter(Boolean));
|
|
1068
|
+
const picks = {};
|
|
1069
|
+
const autoKeys = /* @__PURE__ */ new Set();
|
|
1070
|
+
const empty = dims.filter((d) => !existing[d.key]).sort((a, b) => a.required === b.required ? 0 : a.required ? -1 : 1);
|
|
1071
|
+
for (const d of empty) {
|
|
1072
|
+
let bestId = null;
|
|
1073
|
+
let bestScore = AUTO_THRESHOLD - 1;
|
|
1074
|
+
for (const s of segments) {
|
|
1075
|
+
if (used.has(s.id)) continue;
|
|
1076
|
+
const sc = score(d, s.label);
|
|
1077
|
+
if (sc > bestScore) {
|
|
1078
|
+
bestScore = sc;
|
|
1079
|
+
bestId = s.id;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
if (bestId) {
|
|
1083
|
+
picks[d.key] = bestId;
|
|
1084
|
+
used.add(bestId);
|
|
1085
|
+
autoKeys.add(d.key);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
return { picks, autoKeys };
|
|
1089
|
+
}
|
|
1032
1090
|
function UnifiedSegmentMapper({
|
|
1033
1091
|
segments,
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
automotiveSlots,
|
|
1037
|
-
transformRules,
|
|
1038
|
-
transformRuleLabels,
|
|
1092
|
+
mappableDimensions,
|
|
1093
|
+
initialMappings,
|
|
1039
1094
|
onSave,
|
|
1040
|
-
saving
|
|
1041
|
-
hierarchyMode = "tree",
|
|
1042
|
-
baseHierarchyHint
|
|
1095
|
+
saving
|
|
1043
1096
|
}) {
|
|
1044
|
-
const
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1097
|
+
const dims = useMemo3(
|
|
1098
|
+
() => mappableDimensions?.length ? mappableDimensions : CANONICAL_MAPPABLE,
|
|
1099
|
+
[mappableDimensions]
|
|
1100
|
+
);
|
|
1101
|
+
const [assign, setAssign] = useState4({});
|
|
1102
|
+
const [savedAssign, setSavedAssign] = useState4({});
|
|
1103
|
+
const [autoKeys, setAutoKeys] = useState4(/* @__PURE__ */ new Set());
|
|
1104
|
+
const [activeDim, setActiveDim] = useState4(null);
|
|
1105
|
+
const [query, setQuery] = useState4("");
|
|
1048
1106
|
const [dragging, setDragging] = useState4(null);
|
|
1049
|
-
const [
|
|
1107
|
+
const [dropDim, setDropDim] = useState4(null);
|
|
1050
1108
|
useEffect4(() => {
|
|
1051
|
-
const
|
|
1052
|
-
|
|
1053
|
-
|
|
1109
|
+
const existing = {};
|
|
1110
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
1111
|
+
for (const d of dims) {
|
|
1112
|
+
const segId = initialMappings.find((m) => m.targetCategory === d.category && m.targetKey === d.targetKey)?.segmentId ?? "";
|
|
1113
|
+
existing[d.key] = segId && !claimed.has(segId) ? segId : "";
|
|
1114
|
+
if (existing[d.key]) claimed.add(segId);
|
|
1054
1115
|
}
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
transformRule: existing?.transformRule ?? slot.defaultTransform ?? null
|
|
1062
|
-
};
|
|
1063
|
-
}
|
|
1064
|
-
setAutoAssign(initAuto);
|
|
1065
|
-
}, [hierarchyInitial, automotiveInitial, automotiveSlots]);
|
|
1116
|
+
const { picks, autoKeys: keys } = autoMatch(dims, segments, existing);
|
|
1117
|
+
setAssign({ ...existing, ...picks });
|
|
1118
|
+
setSavedAssign(existing);
|
|
1119
|
+
setAutoKeys(keys);
|
|
1120
|
+
setActiveDim(null);
|
|
1121
|
+
}, [initialMappings, mappableDimensions, segments]);
|
|
1066
1122
|
const segmentById = useMemo3(() => new Map(segments.map((s) => [s.id, s])), [segments]);
|
|
1067
|
-
const usedIds = useMemo3(() =>
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
for (const a of Object.values(autoAssign)) if (a.segmentId) s.add(a.segmentId);
|
|
1071
|
-
return s;
|
|
1072
|
-
}, [hierAssign, autoAssign]);
|
|
1073
|
-
const poolSegments = useMemo3(() => {
|
|
1074
|
-
const q = searchQuery.trim().toLowerCase();
|
|
1123
|
+
const usedIds = useMemo3(() => new Set(Object.values(assign).filter(Boolean)), [assign]);
|
|
1124
|
+
const pool = useMemo3(() => {
|
|
1125
|
+
const q = query.trim().toLowerCase();
|
|
1075
1126
|
return segments.filter((s) => !usedIds.has(s.id)).filter((s) => !q || s.label.toLowerCase().includes(q)).sort((a, b) => a.label.localeCompare(b.label));
|
|
1076
|
-
}, [segments, usedIds,
|
|
1077
|
-
const
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
const errors = [];
|
|
1089
|
-
for (let n = 2; n <= 6; n++) {
|
|
1090
|
-
if (hierAssign[n] && !hierAssign[n - 1]) {
|
|
1091
|
-
errors.push(`${DEFAULT_LEVEL_NAMES[n]} requires ${DEFAULT_LEVEL_NAMES[n - 1]} to be set first`);
|
|
1092
|
-
}
|
|
1093
|
-
}
|
|
1094
|
-
return errors;
|
|
1095
|
-
}, [hierAssign, isSparse]);
|
|
1096
|
-
const missingAutomotive = useMemo3(() => {
|
|
1097
|
-
const out = [];
|
|
1098
|
-
for (const slot of automotiveSlots) {
|
|
1099
|
-
if (hierarchyCoveredKeys.has(slot.key)) continue;
|
|
1100
|
-
const explicitRequired = slot.required || slot.hierarchyCoveredByLevel != null && hierAssign[slot.hierarchyCoveredByLevel] == null;
|
|
1101
|
-
if (explicitRequired && !autoAssign[slot.key]?.segmentId) out.push(slot.label);
|
|
1102
|
-
}
|
|
1103
|
-
return out;
|
|
1104
|
-
}, [automotiveSlots, hierarchyCoveredKeys, hierAssign, autoAssign]);
|
|
1105
|
-
const isDirty = useMemo3(() => {
|
|
1106
|
-
for (const lv of LEVELS) {
|
|
1107
|
-
const cur = hierAssign[lv] ?? null;
|
|
1108
|
-
const saved = hierarchyInitial.find((m) => m.targetKey === `level${lv}`)?.segmentId ?? null;
|
|
1109
|
-
if (cur !== saved) return true;
|
|
1110
|
-
}
|
|
1111
|
-
for (const slot of automotiveSlots) {
|
|
1112
|
-
const cur = autoAssign[slot.key] ?? { segmentId: null, transformRule: null };
|
|
1113
|
-
const saved = automotiveInitial.find((m) => m.targetKey === slot.key);
|
|
1114
|
-
const savedSeg = saved?.segmentId ?? null;
|
|
1115
|
-
const savedRule = saved?.transformRule ?? slot.defaultTransform ?? null;
|
|
1116
|
-
if (cur.segmentId !== savedSeg || cur.transformRule !== savedRule) return true;
|
|
1117
|
-
}
|
|
1118
|
-
return false;
|
|
1119
|
-
}, [hierAssign, autoAssign, hierarchyInitial, automotiveInitial, automotiveSlots]);
|
|
1120
|
-
const canSave = isDirty && hierarchyErrors.length === 0 && missingAutomotive.length === 0 && !saving;
|
|
1121
|
-
function startDrag(payload) {
|
|
1122
|
-
setDragging(payload);
|
|
1123
|
-
}
|
|
1124
|
-
function endDrag() {
|
|
1125
|
-
setDragging(null);
|
|
1126
|
-
setDropTarget(null);
|
|
1127
|
-
}
|
|
1128
|
-
function dropOnHier(level, payload) {
|
|
1129
|
-
const parts = payload.split(":");
|
|
1130
|
-
const kind = parts[0];
|
|
1131
|
-
const segId = parts[2] ?? parts[1];
|
|
1132
|
-
setHierAssign((prev) => {
|
|
1133
|
-
const next = { ...prev };
|
|
1134
|
-
const displaced = next[level] ?? null;
|
|
1135
|
-
next[level] = segId;
|
|
1136
|
-
if (kind === "hier") {
|
|
1137
|
-
const fromLevel = parseInt(parts[1], 10);
|
|
1138
|
-
next[fromLevel] = displaced;
|
|
1127
|
+
}, [segments, usedIds, query]);
|
|
1128
|
+
const required = useMemo3(() => dims.filter((d) => d.required), [dims]);
|
|
1129
|
+
const optional = useMemo3(() => dims.filter((d) => !d.required), [dims]);
|
|
1130
|
+
const missingRequired = required.filter((d) => !assign[d.key]).map((d) => d.label);
|
|
1131
|
+
const autoCount = useMemo3(() => [...autoKeys].filter((k) => !!assign[k]).length, [autoKeys, assign]);
|
|
1132
|
+
const dirty = dims.some((d) => (assign[d.key] ?? "") !== (savedAssign[d.key] ?? ""));
|
|
1133
|
+
const canSave = dirty && missingRequired.length === 0 && !saving;
|
|
1134
|
+
function setOne(dimKey, segId) {
|
|
1135
|
+
setAssign((p) => {
|
|
1136
|
+
const next = { ...p, [dimKey]: segId };
|
|
1137
|
+
if (segId) {
|
|
1138
|
+
for (const k of Object.keys(next)) if (k !== dimKey && next[k] === segId) next[k] = "";
|
|
1139
1139
|
}
|
|
1140
1140
|
return next;
|
|
1141
1141
|
});
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
}
|
|
1148
|
-
function dropOnAuto(slotKey, payload) {
|
|
1149
|
-
const parts = payload.split(":");
|
|
1150
|
-
const kind = parts[0];
|
|
1151
|
-
const segId = parts[2] ?? parts[1];
|
|
1152
|
-
setAutoAssign((prev) => {
|
|
1153
|
-
const slot = automotiveSlots.find((s) => s.key === slotKey);
|
|
1154
|
-
return {
|
|
1155
|
-
...prev,
|
|
1156
|
-
[slotKey]: { segmentId: segId, transformRule: prev[slotKey]?.transformRule ?? slot?.defaultTransform ?? null }
|
|
1157
|
-
};
|
|
1142
|
+
setAutoKeys((prev) => {
|
|
1143
|
+
if (!prev.has(dimKey)) return prev;
|
|
1144
|
+
const next = new Set(prev);
|
|
1145
|
+
next.delete(dimKey);
|
|
1146
|
+
return next;
|
|
1158
1147
|
});
|
|
1159
|
-
if (kind === "hier") {
|
|
1160
|
-
const fromLevel = parseInt(parts[1], 10);
|
|
1161
|
-
setHierAssign((p) => ({ ...p, [fromLevel]: null }));
|
|
1162
|
-
} else if (kind === "auto") {
|
|
1163
|
-
const fromKey = parts[1];
|
|
1164
|
-
if (fromKey !== slotKey) {
|
|
1165
|
-
setAutoAssign((p) => ({ ...p, [fromKey]: { segmentId: null, transformRule: p[fromKey]?.transformRule ?? null } }));
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
|
-
endDrag();
|
|
1169
1148
|
}
|
|
1170
|
-
function
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
setHierAssign((p) => ({ ...p, [lv]: null }));
|
|
1176
|
-
} else if (kind === "auto") {
|
|
1177
|
-
const slotKey = parts[1];
|
|
1178
|
-
setAutoAssign((p) => ({ ...p, [slotKey]: { segmentId: null, transformRule: p[slotKey]?.transformRule ?? null } }));
|
|
1179
|
-
}
|
|
1180
|
-
endDrag();
|
|
1181
|
-
}
|
|
1182
|
-
function clearAuto(slotKey) {
|
|
1183
|
-
setAutoAssign((p) => ({ ...p, [slotKey]: { segmentId: null, transformRule: p[slotKey]?.transformRule ?? null } }));
|
|
1184
|
-
}
|
|
1185
|
-
function setAutoTransform(slotKey, rule) {
|
|
1186
|
-
setAutoAssign((p) => ({ ...p, [slotKey]: { ...p[slotKey], transformRule: rule } }));
|
|
1149
|
+
function clickColumn(segId) {
|
|
1150
|
+
if (!activeDim) return;
|
|
1151
|
+
setOne(activeDim, segId);
|
|
1152
|
+
const nextEmpty = [...required, ...optional].find((d) => d.key !== activeDim && !assign[d.key] && d.required);
|
|
1153
|
+
setActiveDim(nextEmpty?.key ?? null);
|
|
1187
1154
|
}
|
|
1188
1155
|
async function handleSave() {
|
|
1189
|
-
const
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1156
|
+
const hierarchy = [];
|
|
1157
|
+
const automotive = [];
|
|
1158
|
+
const dimensions = [];
|
|
1159
|
+
const managedH = new Set(dims.filter((d) => d.category === "hierarchy_level").map((d) => d.targetKey));
|
|
1160
|
+
const managedA = new Set(dims.filter((d) => d.category === "automotive_field").map((d) => d.targetKey));
|
|
1161
|
+
const managedD = new Set(dims.filter((d) => d.category === "dimension").map((d) => d.targetKey));
|
|
1162
|
+
const existingTransform = /* @__PURE__ */ new Map();
|
|
1163
|
+
for (const m of initialMappings) {
|
|
1164
|
+
if (m.targetCategory === "automotive_field") existingTransform.set(m.targetKey, m.transformRule);
|
|
1165
|
+
if (m.targetCategory === "hierarchy_level" && !managedH.has(m.targetKey))
|
|
1166
|
+
hierarchy.push({ targetKey: m.targetKey, segmentId: m.segmentId });
|
|
1167
|
+
else if (m.targetCategory === "automotive_field" && !managedA.has(m.targetKey))
|
|
1168
|
+
automotive.push({ targetKey: m.targetKey, segmentId: m.segmentId, transformRule: m.transformRule });
|
|
1169
|
+
else if (m.targetCategory === "dimension" && !managedD.has(m.targetKey))
|
|
1170
|
+
dimensions.push({ targetKey: m.targetKey, segmentId: m.segmentId });
|
|
1193
1171
|
}
|
|
1194
|
-
const
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
if (
|
|
1198
|
-
|
|
1199
|
-
|
|
1172
|
+
for (const d of dims) {
|
|
1173
|
+
const segmentId = assign[d.key];
|
|
1174
|
+
if (!segmentId) continue;
|
|
1175
|
+
if (d.category === "hierarchy_level") hierarchy.push({ targetKey: d.targetKey, segmentId });
|
|
1176
|
+
else if (d.category === "automotive_field")
|
|
1177
|
+
automotive.push({
|
|
1178
|
+
targetKey: d.targetKey,
|
|
1179
|
+
segmentId,
|
|
1180
|
+
transformRule: existingTransform.has(d.targetKey) ? existingTransform.get(d.targetKey) : autoTransform(d.targetKey)
|
|
1181
|
+
});
|
|
1182
|
+
else dimensions.push({ targetKey: d.targetKey, segmentId });
|
|
1200
1183
|
}
|
|
1201
|
-
await onSave(
|
|
1184
|
+
await onSave(hierarchy, automotive, dimensions);
|
|
1185
|
+
setSavedAssign({ ...assign });
|
|
1186
|
+
setAutoKeys(/* @__PURE__ */ new Set());
|
|
1187
|
+
}
|
|
1188
|
+
function renderRow(d) {
|
|
1189
|
+
const segId = assign[d.key];
|
|
1190
|
+
const seg = segId ? segmentById.get(segId) : void 0;
|
|
1191
|
+
const isActive = activeDim === d.key;
|
|
1192
|
+
const isDrop = dropDim === d.key;
|
|
1193
|
+
return /* @__PURE__ */ jsxs5(
|
|
1194
|
+
"div",
|
|
1195
|
+
{
|
|
1196
|
+
className: `cfi-vi-hier-row${isActive ? " cfi-vi-dropzone--active" : ""}`,
|
|
1197
|
+
style: { cursor: "pointer", borderRadius: 6 },
|
|
1198
|
+
onClick: () => setActiveDim(isActive ? null : d.key),
|
|
1199
|
+
onDragOver: (e) => {
|
|
1200
|
+
e.preventDefault();
|
|
1201
|
+
setDropDim(d.key);
|
|
1202
|
+
},
|
|
1203
|
+
onDragLeave: (e) => {
|
|
1204
|
+
if (!e.currentTarget.contains(e.relatedTarget)) setDropDim(null);
|
|
1205
|
+
},
|
|
1206
|
+
onDrop: (e) => {
|
|
1207
|
+
e.preventDefault();
|
|
1208
|
+
if (dragging) {
|
|
1209
|
+
setOne(d.key, dragging);
|
|
1210
|
+
setDragging(null);
|
|
1211
|
+
}
|
|
1212
|
+
setDropDim(null);
|
|
1213
|
+
},
|
|
1214
|
+
children: [
|
|
1215
|
+
/* @__PURE__ */ jsx5("div", { className: `cfi-vi-hier-dot ${seg ? "cfi-vi-hier-dot--filled" : ""}` }),
|
|
1216
|
+
/* @__PURE__ */ jsx5("span", { className: "cfi-vi-hier-name", style: { minWidth: 150 }, children: d.label }),
|
|
1217
|
+
d.required && !seg && /* @__PURE__ */ jsx5(Badge, { variant: "outline", children: "required" }),
|
|
1218
|
+
/* @__PURE__ */ jsx5("div", { className: `cfi-vi-dropzone ${isDrop ? "cfi-vi-dropzone--active" : seg ? "cfi-vi-dropzone--filled" : "cfi-vi-dropzone--empty"}`, style: { flex: 1 }, children: seg ? /* @__PURE__ */ jsxs5("div", { className: "cfi-vi-chip--assigned", children: [
|
|
1219
|
+
/* @__PURE__ */ jsx5("div", { className: "cfi-vi-chip-dot cfi-vi-chip-dot--on" }),
|
|
1220
|
+
/* @__PURE__ */ jsx5("span", { className: "cfi-vi-chip-label", children: seg.label }),
|
|
1221
|
+
autoKeys.has(d.key) && /* @__PURE__ */ jsx5("span", { className: "cfi-vi-auto-auto-tag", children: "auto" }),
|
|
1222
|
+
/* @__PURE__ */ jsx5(
|
|
1223
|
+
"button",
|
|
1224
|
+
{
|
|
1225
|
+
type: "button",
|
|
1226
|
+
className: "cfi-vi-chip-x",
|
|
1227
|
+
title: "Unassign \u2014 returns to pool",
|
|
1228
|
+
onClick: (e) => {
|
|
1229
|
+
e.stopPropagation();
|
|
1230
|
+
setOne(d.key, "");
|
|
1231
|
+
},
|
|
1232
|
+
children: "\xD7"
|
|
1233
|
+
}
|
|
1234
|
+
)
|
|
1235
|
+
] }) : /* @__PURE__ */ jsx5("span", { className: "cfi-vi-dropzone-ph", children: isActive ? "Click a column \u2192" : isDrop ? "Drop here" : "Drag or click a column\u2026" }) })
|
|
1236
|
+
]
|
|
1237
|
+
},
|
|
1238
|
+
d.key
|
|
1239
|
+
);
|
|
1240
|
+
}
|
|
1241
|
+
if (segments.length === 0) {
|
|
1242
|
+
return /* @__PURE__ */ jsx5("div", { className: "cfi-vi", children: /* @__PURE__ */ jsx5("p", { className: "cfi-vi-pool-empty", children: "No columns detected. Run \u201CDetect segments\u201D from the Structure tab first." }) });
|
|
1202
1243
|
}
|
|
1203
|
-
void transformRules;
|
|
1204
1244
|
return /* @__PURE__ */ jsxs5("div", { className: "cfi-vi cfi-vi-mapper", children: [
|
|
1205
1245
|
/* @__PURE__ */ jsxs5("div", { className: "cfi-vi-mapper-col", children: [
|
|
1246
|
+
/* @__PURE__ */ jsx5("div", { className: "cfi-vi-mapper-section-head", children: /* @__PURE__ */ jsxs5("span", { className: "cfi-vi-mapper-note", style: { display: "inline-flex", alignItems: "center", gap: 6 }, children: [
|
|
1247
|
+
/* @__PURE__ */ jsx5(Wand2, { size: 13, "aria-hidden": true }),
|
|
1248
|
+
" Auto-matched ",
|
|
1249
|
+
autoCount,
|
|
1250
|
+
" \xB7 ",
|
|
1251
|
+
missingRequired.length,
|
|
1252
|
+
" required still need a column"
|
|
1253
|
+
] }) }),
|
|
1206
1254
|
/* @__PURE__ */ jsxs5("section", { className: "cfi-vi-mapper-section", children: [
|
|
1207
|
-
/* @__PURE__ */
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
HierarchyRow,
|
|
1213
|
-
{
|
|
1214
|
-
level,
|
|
1215
|
-
segId: hierAssign[level] ?? null,
|
|
1216
|
-
segmentById,
|
|
1217
|
-
isDropTarget: dropTarget === `hier:${level}`,
|
|
1218
|
-
onDragOver: () => setDropTarget(`hier:${level}`),
|
|
1219
|
-
onDragLeave: () => setDropTarget(null),
|
|
1220
|
-
onDrop: () => dragging && dropOnHier(level, dragging),
|
|
1221
|
-
onChipDragStart: (segId) => startDrag(`hier:${level}:${segId}`),
|
|
1222
|
-
onChipDragEnd: endDrag,
|
|
1223
|
-
onChipRemove: () => setHierAssign((p) => ({ ...p, [level]: null })),
|
|
1224
|
-
sparse: isSparse,
|
|
1225
|
-
baseHint: baseHierarchyHint?.[level]
|
|
1226
|
-
},
|
|
1227
|
-
level
|
|
1228
|
-
)) }),
|
|
1229
|
-
hierarchyErrors.length > 0 && /* @__PURE__ */ jsx5("div", { children: hierarchyErrors.map((err) => /* @__PURE__ */ jsxs5("p", { className: "cfi-vi-mapper-err", children: [
|
|
1230
|
-
"\u26A0 ",
|
|
1231
|
-
err
|
|
1232
|
-
] }, err)) })
|
|
1233
|
-
] }),
|
|
1234
|
-
automotiveSlots.length > 0 && /* @__PURE__ */ jsxs5("section", { className: "cfi-vi-mapper-section", children: [
|
|
1235
|
-
/* @__PURE__ */ jsxs5("div", { className: "cfi-vi-mapper-section-head", children: [
|
|
1236
|
-
/* @__PURE__ */ jsx5("h3", { className: "cfi-vi-mapper-h3", children: "Automotive Fields" }),
|
|
1237
|
-
/* @__PURE__ */ jsx5("span", { className: "cfi-vi-mapper-note", children: "Brand / Nameplate / Plant Name come from the hierarchy unless overridden." })
|
|
1238
|
-
] }),
|
|
1239
|
-
/* @__PURE__ */ jsx5("div", { className: "cfi-vi-mapper-box", children: automotiveSlots.map((slot) => {
|
|
1240
|
-
const covered = hierarchyCoveredKeys.has(slot.key);
|
|
1241
|
-
const hierarchySegId = slot.hierarchyCoveredByLevel != null ? hierAssign[slot.hierarchyCoveredByLevel] : null;
|
|
1242
|
-
const assignment = autoAssign[slot.key];
|
|
1243
|
-
return /* @__PURE__ */ jsx5(
|
|
1244
|
-
AutomotiveRow,
|
|
1245
|
-
{
|
|
1246
|
-
slot,
|
|
1247
|
-
covered,
|
|
1248
|
-
hierarchySegment: covered && hierarchySegId ? segmentById.get(hierarchySegId) : void 0,
|
|
1249
|
-
assignment,
|
|
1250
|
-
segmentById,
|
|
1251
|
-
transformRuleLabels,
|
|
1252
|
-
hierarchyEmpty: slot.hierarchyCoveredByLevel != null && !hierAssign[slot.hierarchyCoveredByLevel],
|
|
1253
|
-
isDropTarget: dropTarget === `auto:${slot.key}`,
|
|
1254
|
-
onDragOver: () => setDropTarget(`auto:${slot.key}`),
|
|
1255
|
-
onDragLeave: () => setDropTarget(null),
|
|
1256
|
-
onDrop: () => dragging && dropOnAuto(slot.key, dragging),
|
|
1257
|
-
onChipDragStart: (segId) => startDrag(`auto:${slot.key}:${segId}`),
|
|
1258
|
-
onChipDragEnd: endDrag,
|
|
1259
|
-
onChipRemove: () => clearAuto(slot.key),
|
|
1260
|
-
onTransformChange: (rule) => setAutoTransform(slot.key, rule)
|
|
1261
|
-
},
|
|
1262
|
-
slot.key
|
|
1263
|
-
);
|
|
1264
|
-
}) }),
|
|
1265
|
-
missingAutomotive.length > 0 && /* @__PURE__ */ jsxs5("p", { className: "cfi-vi-mapper-err", children: [
|
|
1266
|
-
"\u26A0 Required: ",
|
|
1267
|
-
missingAutomotive.join(", ")
|
|
1255
|
+
/* @__PURE__ */ jsx5("div", { className: "cfi-vi-mapper-section-head", children: /* @__PURE__ */ jsx5("h3", { className: "cfi-vi-mapper-h3", children: "Required" }) }),
|
|
1256
|
+
/* @__PURE__ */ jsx5("div", { className: "cfi-vi-mapper-box", children: required.map(renderRow) }),
|
|
1257
|
+
missingRequired.length > 0 && /* @__PURE__ */ jsxs5("p", { className: "cfi-vi-mapper-err", children: [
|
|
1258
|
+
"\u26A0 Still needed: ",
|
|
1259
|
+
missingRequired.join(", ")
|
|
1268
1260
|
] })
|
|
1269
1261
|
] }),
|
|
1262
|
+
optional.length > 0 && /* @__PURE__ */ jsxs5("section", { className: "cfi-vi-mapper-section", children: [
|
|
1263
|
+
/* @__PURE__ */ jsx5("div", { className: "cfi-vi-mapper-section-head", children: /* @__PURE__ */ jsx5("h3", { className: "cfi-vi-mapper-h3", children: "Optional" }) }),
|
|
1264
|
+
/* @__PURE__ */ jsx5("div", { className: "cfi-vi-mapper-box", children: optional.map(renderRow) })
|
|
1265
|
+
] }),
|
|
1270
1266
|
/* @__PURE__ */ jsxs5("div", { className: "cfi-vi-mapper-save", children: [
|
|
1271
1267
|
/* @__PURE__ */ jsx5(Btn, { size: "sm", onClick: handleSave, disabled: !canSave, children: saving ? "Saving\u2026" : "Save Mapping" }),
|
|
1272
|
-
|
|
1273
|
-
!
|
|
1268
|
+
dirty && missingRequired.length === 0 && !saving && /* @__PURE__ */ jsx5("span", { className: "cfi-vi-mapper-dirty", children: "Unsaved changes" }),
|
|
1269
|
+
!dirty && /* @__PURE__ */ jsx5("span", { className: "cfi-vi-muted", children: "Up to date" })
|
|
1274
1270
|
] })
|
|
1275
1271
|
] }),
|
|
1276
1272
|
/* @__PURE__ */ jsxs5("aside", { className: "cfi-vi-pool-col", children: [
|
|
1277
1273
|
/* @__PURE__ */ jsxs5("div", { className: "cfi-vi-mapper-section-head", children: [
|
|
1278
|
-
/* @__PURE__ */ jsx5("h3", { className: "cfi-vi-mapper-h3", children: "
|
|
1274
|
+
/* @__PURE__ */ jsx5("h3", { className: "cfi-vi-mapper-h3", children: "Columns" }),
|
|
1279
1275
|
/* @__PURE__ */ jsxs5("span", { className: "cfi-vi-mapper-note", children: [
|
|
1280
|
-
|
|
1276
|
+
pool.length,
|
|
1281
1277
|
" of ",
|
|
1282
1278
|
segments.length,
|
|
1283
1279
|
" unused"
|
|
@@ -1285,158 +1281,29 @@ function UnifiedSegmentMapper({
|
|
|
1285
1281
|
] }),
|
|
1286
1282
|
/* @__PURE__ */ jsxs5("div", { className: "cfi-vi-search", children: [
|
|
1287
1283
|
/* @__PURE__ */ jsx5(Search, { size: 14, className: "cfi-vi-search-icon", "aria-hidden": true }),
|
|
1288
|
-
/* @__PURE__ */ jsx5(TextInput, { type: "search", placeholder: "Filter
|
|
1284
|
+
/* @__PURE__ */ jsx5(TextInput, { type: "search", placeholder: "Filter columns\u2026", value: query, onChange: (e) => setQuery(e.target.value) })
|
|
1289
1285
|
] }),
|
|
1290
|
-
/* @__PURE__ */ jsx5(
|
|
1286
|
+
/* @__PURE__ */ jsx5("p", { className: "cfi-vi-mapper-note", children: activeDim ? `Click a column to assign it to \u201C${dims.find((d) => d.key === activeDim)?.label}\u201D.` : "Click a dimension to select it, then click a column \u2014 or drag a column onto a dimension." }),
|
|
1287
|
+
/* @__PURE__ */ jsx5("div", { className: "cfi-vi-pool", children: pool.length === 0 ? /* @__PURE__ */ jsx5("p", { className: "cfi-vi-pool-empty", children: query ? "No columns match the filter." : "All columns assigned." }) : /* @__PURE__ */ jsx5("div", { className: "cfi-vi-chips", children: pool.map((s) => /* @__PURE__ */ jsxs5(
|
|
1291
1288
|
"div",
|
|
1292
1289
|
{
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
},
|
|
1305
|
-
children: segments.length === 0 ? /* @__PURE__ */ jsx5("p", { className: "cfi-vi-pool-empty", children: "No segments detected. Run \u201CDetect segments\u201D from the source page first." }) : poolSegments.length === 0 ? /* @__PURE__ */ jsx5("p", { className: "cfi-vi-pool-empty", children: searchQuery ? "No segments match the filter." : "All segments assigned \u2014 drop a chip here to return it." }) : /* @__PURE__ */ jsx5("div", { className: "cfi-vi-chips", children: poolSegments.map((seg) => /* @__PURE__ */ jsx5(SegmentChip, { label: seg.label, onDragStart: () => startDrag(`pool:${seg.id}`), onDragEnd: endDrag }, seg.id)) })
|
|
1306
|
-
}
|
|
1307
|
-
),
|
|
1308
|
-
/* @__PURE__ */ jsx5("p", { className: "cfi-vi-mapper-note", children: "Drag chips onto a hierarchy level or an automotive field. Drop a chip back here to unassign." })
|
|
1309
|
-
] })
|
|
1310
|
-
] });
|
|
1311
|
-
}
|
|
1312
|
-
function HierarchyRow({
|
|
1313
|
-
level,
|
|
1314
|
-
segId,
|
|
1315
|
-
segmentById,
|
|
1316
|
-
isDropTarget,
|
|
1317
|
-
onDragOver,
|
|
1318
|
-
onDragLeave,
|
|
1319
|
-
onDrop,
|
|
1320
|
-
onChipDragStart,
|
|
1321
|
-
onChipDragEnd,
|
|
1322
|
-
onChipRemove,
|
|
1323
|
-
sparse,
|
|
1324
|
-
baseHint
|
|
1325
|
-
}) {
|
|
1326
|
-
const seg = segId ? segmentById.get(segId) : void 0;
|
|
1327
|
-
const isLeaf = level === 6;
|
|
1328
|
-
const indent = sparse ? 0 : (level - 1) * 22;
|
|
1329
|
-
const levelName = DEFAULT_LEVEL_NAMES[level];
|
|
1330
|
-
return /* @__PURE__ */ jsxs5("div", { className: "cfi-vi-hier-row", style: { paddingLeft: `${indent}px` }, children: [
|
|
1331
|
-
!sparse && level > 1 && /* @__PURE__ */ jsx5("div", { className: "cfi-vi-hier-elbow" }),
|
|
1332
|
-
/* @__PURE__ */ jsx5("div", { className: `cfi-vi-hier-dot ${seg ? "cfi-vi-hier-dot--filled" : ""}` }),
|
|
1333
|
-
/* @__PURE__ */ jsx5("span", { className: "cfi-vi-hier-name", children: levelName }),
|
|
1334
|
-
isLeaf && !sparse && /* @__PURE__ */ jsx5("span", { className: "cfi-vi-leaf", children: "Leaf" }),
|
|
1335
|
-
/* @__PURE__ */ jsx5(
|
|
1336
|
-
"div",
|
|
1337
|
-
{
|
|
1338
|
-
className: `cfi-vi-dropzone ${isDropTarget ? "cfi-vi-dropzone--active" : seg ? "cfi-vi-dropzone--filled" : "cfi-vi-dropzone--empty"}`,
|
|
1339
|
-
onDragOver: (e) => {
|
|
1340
|
-
e.preventDefault();
|
|
1341
|
-
onDragOver();
|
|
1342
|
-
},
|
|
1343
|
-
onDragLeave: (e) => {
|
|
1344
|
-
if (!e.currentTarget.contains(e.relatedTarget)) onDragLeave();
|
|
1345
|
-
},
|
|
1346
|
-
onDrop: (e) => {
|
|
1347
|
-
e.preventDefault();
|
|
1348
|
-
onDrop();
|
|
1349
|
-
},
|
|
1350
|
-
children: seg ? /* @__PURE__ */ jsx5(AssignedChip, { seg, onDragStart: () => onChipDragStart(seg.id), onDragEnd: onChipDragEnd, onRemove: onChipRemove }) : /* @__PURE__ */ jsx5("span", { className: "cfi-vi-dropzone-ph", children: isDropTarget ? "Drop here" : "Drop a segment\u2026" })
|
|
1351
|
-
}
|
|
1352
|
-
),
|
|
1353
|
-
baseHint && /* @__PURE__ */ jsxs5("span", { className: "cfi-vi-base-hint", title: `Base source uses "${baseHint}" on ${levelName}. Match here for the overlay to join.`, children: [
|
|
1354
|
-
"base: ",
|
|
1355
|
-
baseHint
|
|
1356
|
-
] })
|
|
1357
|
-
] });
|
|
1358
|
-
}
|
|
1359
|
-
function AutomotiveRow({
|
|
1360
|
-
slot,
|
|
1361
|
-
covered,
|
|
1362
|
-
hierarchySegment,
|
|
1363
|
-
assignment,
|
|
1364
|
-
segmentById,
|
|
1365
|
-
transformRuleLabels,
|
|
1366
|
-
hierarchyEmpty,
|
|
1367
|
-
isDropTarget,
|
|
1368
|
-
onDragOver,
|
|
1369
|
-
onDragLeave,
|
|
1370
|
-
onDrop,
|
|
1371
|
-
onChipDragStart,
|
|
1372
|
-
onChipDragEnd,
|
|
1373
|
-
onChipRemove
|
|
1374
|
-
}) {
|
|
1375
|
-
const assignedSeg = assignment?.segmentId ? segmentById.get(assignment.segmentId) : void 0;
|
|
1376
|
-
const requiredBadge = (slot.required || hierarchyEmpty) && !covered && !assignedSeg;
|
|
1377
|
-
return /* @__PURE__ */ jsxs5("div", { className: "cfi-vi-auto-row", children: [
|
|
1378
|
-
/* @__PURE__ */ jsxs5("div", { className: "cfi-vi-auto-label", children: [
|
|
1379
|
-
/* @__PURE__ */ jsxs5("div", { className: "cfi-vi-row", children: [
|
|
1380
|
-
/* @__PURE__ */ jsx5("span", { className: "cfi-vi-auto-label-text", children: slot.label }),
|
|
1381
|
-
requiredBadge && /* @__PURE__ */ jsx5(Badge, { variant: "outline", children: "required" })
|
|
1382
|
-
] }),
|
|
1383
|
-
slot.hierarchyCoveredByLevel != null && /* @__PURE__ */ jsxs5("p", { className: `cfi-vi-auto-cover ${covered ? "cfi-vi-auto-cover--on" : ""}`, children: [
|
|
1384
|
-
covered ? "\u2713 " : "",
|
|
1385
|
-
"From L",
|
|
1386
|
-
slot.hierarchyCoveredByLevel,
|
|
1387
|
-
" hierarchy"
|
|
1388
|
-
] })
|
|
1389
|
-
] }),
|
|
1390
|
-
/* @__PURE__ */ jsx5(
|
|
1391
|
-
"div",
|
|
1392
|
-
{
|
|
1393
|
-
className: `cfi-vi-dropzone ${isDropTarget ? "cfi-vi-dropzone--active" : assignedSeg ? "cfi-vi-dropzone--filled" : covered ? "cfi-vi-dropzone--covered" : "cfi-vi-dropzone--empty"}`,
|
|
1394
|
-
onDragOver: (e) => {
|
|
1395
|
-
e.preventDefault();
|
|
1396
|
-
onDragOver();
|
|
1397
|
-
},
|
|
1398
|
-
onDragLeave: (e) => {
|
|
1399
|
-
if (!e.currentTarget.contains(e.relatedTarget)) onDragLeave();
|
|
1400
|
-
},
|
|
1401
|
-
onDrop: (e) => {
|
|
1402
|
-
e.preventDefault();
|
|
1403
|
-
onDrop();
|
|
1290
|
+
draggable: true,
|
|
1291
|
+
onDragStart: () => setDragging(s.id),
|
|
1292
|
+
onDragEnd: () => setDragging(null),
|
|
1293
|
+
onClick: () => clickColumn(s.id),
|
|
1294
|
+
className: "cfi-vi-chip",
|
|
1295
|
+
style: { cursor: activeDim ? "pointer" : "grab" },
|
|
1296
|
+
title: activeDim ? "Click to assign" : "Drag onto a dimension, or select a dimension first",
|
|
1297
|
+
children: [
|
|
1298
|
+
/* @__PURE__ */ jsx5("div", { className: "cfi-vi-chip-dot" }),
|
|
1299
|
+
s.label
|
|
1300
|
+
]
|
|
1404
1301
|
},
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
/* @__PURE__ */ jsx5("span", { className: "cfi-vi-auto-auto-label", children: hierarchySegment.label }),
|
|
1408
|
-
/* @__PURE__ */ jsx5("span", { className: "cfi-vi-auto-auto-tag", children: "auto" })
|
|
1409
|
-
] }) : /* @__PURE__ */ jsx5("span", { className: "cfi-vi-dropzone-ph", children: isDropTarget ? "Drop here" : "Drop a segment\u2026" })
|
|
1410
|
-
}
|
|
1411
|
-
),
|
|
1412
|
-
slot.defaultTransform && !covered && /* @__PURE__ */ jsxs5("div", { className: "cfi-vi-auto-transform", children: [
|
|
1413
|
-
/* @__PURE__ */ jsx5("span", { className: "cfi-vi-auto-transform-label", children: "Transform:" }),
|
|
1414
|
-
" ",
|
|
1415
|
-
transformRuleLabels[slot.defaultTransform] ?? slot.defaultTransform
|
|
1302
|
+
s.id
|
|
1303
|
+
)) }) })
|
|
1416
1304
|
] })
|
|
1417
1305
|
] });
|
|
1418
1306
|
}
|
|
1419
|
-
function AssignedChip({
|
|
1420
|
-
seg,
|
|
1421
|
-
onDragStart,
|
|
1422
|
-
onDragEnd,
|
|
1423
|
-
onRemove
|
|
1424
|
-
}) {
|
|
1425
|
-
return /* @__PURE__ */ jsxs5("div", { draggable: true, onDragStart, onDragEnd, className: "cfi-vi-chip--assigned", children: [
|
|
1426
|
-
/* @__PURE__ */ jsx5("div", { className: "cfi-vi-chip-dot cfi-vi-chip-dot--on" }),
|
|
1427
|
-
/* @__PURE__ */ jsx5("span", { className: "cfi-vi-chip-label", children: seg.label }),
|
|
1428
|
-
/* @__PURE__ */ jsx5("button", { type: "button", onClick: (e) => {
|
|
1429
|
-
e.stopPropagation();
|
|
1430
|
-
onRemove();
|
|
1431
|
-
}, className: "cfi-vi-chip-x", title: "Remove \u2014 returns to pool", children: "\xD7" })
|
|
1432
|
-
] });
|
|
1433
|
-
}
|
|
1434
|
-
function SegmentChip({ label, onDragStart, onDragEnd }) {
|
|
1435
|
-
return /* @__PURE__ */ jsxs5("div", { draggable: true, onDragStart, onDragEnd, className: "cfi-vi-chip", children: [
|
|
1436
|
-
/* @__PURE__ */ jsx5("div", { className: "cfi-vi-chip-dot" }),
|
|
1437
|
-
label
|
|
1438
|
-
] });
|
|
1439
|
-
}
|
|
1440
1307
|
|
|
1441
1308
|
// src/VolumeSourceDetail.tsx
|
|
1442
1309
|
import { useEffect as useEffect7, useRef as useRef3, useState as useState7 } from "react";
|
|
@@ -1730,8 +1597,8 @@ function VolumeSourceDetail({ transport, sourceId, onOpenSource, onBack, classNa
|
|
|
1730
1597
|
const [mappingKeys, setMappingKeys] = useState7(null);
|
|
1731
1598
|
const [mappingRows, setMappingRows] = useState7([]);
|
|
1732
1599
|
const [savingMapping, setSavingMapping] = useState7(false);
|
|
1733
|
-
const [baseHierarchyHint, setBaseHierarchyHint] = useState7({});
|
|
1734
1600
|
const [requiredStatus, setRequiredStatus] = useState7(null);
|
|
1601
|
+
const [mappableDimensions, setMappableDimensions] = useState7([]);
|
|
1735
1602
|
async function loadRequiredStatus() {
|
|
1736
1603
|
if (!id || !transport.getRequiredMappingStatus) {
|
|
1737
1604
|
setRequiredStatus(null);
|
|
@@ -1762,34 +1629,12 @@ function VolumeSourceDetail({ transport, sourceId, onOpenSource, onBack, classNa
|
|
|
1762
1629
|
setHasData(uploads.items.some((u) => u.status === "done" || u.status === "done_with_errors"));
|
|
1763
1630
|
setMappingKeys(keys);
|
|
1764
1631
|
setMappingRows(mapping.items);
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
if (tenantConfig?.sourceId && tenantConfig.sourceId !== id) {
|
|
1769
|
-
const [baseSrc, baseMapping] = await Promise.all([
|
|
1770
|
-
transport.getSource(tenantConfig.sourceId),
|
|
1771
|
-
transport.listSegmentMapping(tenantConfig.sourceId)
|
|
1772
|
-
]);
|
|
1773
|
-
const baseSegLabel = new Map(baseSrc.segments.map((s) => [s.id, s.label]));
|
|
1774
|
-
const hint = {};
|
|
1775
|
-
for (const row of baseMapping.items) {
|
|
1776
|
-
if (row.targetCategory !== "hierarchy_level") continue;
|
|
1777
|
-
const m = /^level(\d)$/.exec(row.targetKey);
|
|
1778
|
-
if (!m) continue;
|
|
1779
|
-
const label = baseSegLabel.get(row.segmentId);
|
|
1780
|
-
if (label) hint[parseInt(m[1], 10)] = label;
|
|
1781
|
-
}
|
|
1782
|
-
setBaseHierarchyHint(hint);
|
|
1783
|
-
} else {
|
|
1784
|
-
setBaseHierarchyHint({});
|
|
1785
|
-
}
|
|
1786
|
-
} catch {
|
|
1787
|
-
setBaseHierarchyHint({});
|
|
1788
|
-
}
|
|
1632
|
+
void loadRequiredStatus();
|
|
1633
|
+
if (transport.getMappableDimensions) {
|
|
1634
|
+
transport.getMappableDimensions().then(setMappableDimensions).catch(() => setMappableDimensions([]));
|
|
1789
1635
|
} else {
|
|
1790
|
-
|
|
1636
|
+
setMappableDimensions([]);
|
|
1791
1637
|
}
|
|
1792
|
-
void loadRequiredStatus();
|
|
1793
1638
|
} catch (e) {
|
|
1794
1639
|
setError(e instanceof Error ? e.message : "Failed to load source");
|
|
1795
1640
|
} finally {
|
|
@@ -1822,14 +1667,19 @@ function VolumeSourceDetail({ transport, sourceId, onOpenSource, onBack, classNa
|
|
|
1822
1667
|
setError(e instanceof Error ? e.message : "Delete failed");
|
|
1823
1668
|
}
|
|
1824
1669
|
}
|
|
1825
|
-
async function handleSaveMapping(hierarchy, automotive) {
|
|
1670
|
+
async function handleSaveMapping(hierarchy, automotive, dimensions) {
|
|
1826
1671
|
if (!id) return;
|
|
1827
1672
|
setSavingMapping(true);
|
|
1828
1673
|
setError(null);
|
|
1829
1674
|
try {
|
|
1830
1675
|
const hResp = await transport.replaceSegmentMappingCategory(id, "hierarchy_level", hierarchy);
|
|
1831
1676
|
const aResp = await transport.replaceSegmentMappingCategory(id, "automotive_field", automotive);
|
|
1832
|
-
|
|
1677
|
+
const rows = [...hResp.items, ...aResp.items];
|
|
1678
|
+
if (transport.getMappableDimensions) {
|
|
1679
|
+
const dResp = await transport.replaceSegmentMappingCategory(id, "dimension", dimensions);
|
|
1680
|
+
rows.push(...dResp.items);
|
|
1681
|
+
}
|
|
1682
|
+
setMappingRows(rows);
|
|
1833
1683
|
void loadRequiredStatus();
|
|
1834
1684
|
} catch (e) {
|
|
1835
1685
|
setError(e instanceof Error ? e.message : "Failed to save mapping");
|
|
@@ -2181,16 +2031,11 @@ function VolumeSourceDetail({ transport, sourceId, onOpenSource, onBack, classNa
|
|
|
2181
2031
|
source.segments.length === 0 ? /* @__PURE__ */ jsx8("p", { className: "cfi-vi-muted", children: "No segments yet. Add segments on the Structure tab (Detect from file or Add manually) before mapping." }) : /* @__PURE__ */ jsx8(
|
|
2182
2032
|
UnifiedSegmentMapper,
|
|
2183
2033
|
{
|
|
2184
|
-
segments: source.segments
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
automotiveSlots: isOverlay ? [] : AUTOMOTIVE_SLOTS,
|
|
2188
|
-
transformRules: mappingKeys?.transformRules ?? [],
|
|
2189
|
-
transformRuleLabels: AUTOMOTIVE_TRANSFORM_LABELS,
|
|
2034
|
+
segments: source.segments,
|
|
2035
|
+
mappableDimensions,
|
|
2036
|
+
initialMappings: mappingRows,
|
|
2190
2037
|
onSave: handleSaveMapping,
|
|
2191
|
-
saving: savingMapping
|
|
2192
|
-
hierarchyMode: isOverlay ? "sparse" : "tree",
|
|
2193
|
-
baseHierarchyHint: isOverlay ? baseHierarchyHint : void 0
|
|
2038
|
+
saving: savingMapping
|
|
2194
2039
|
}
|
|
2195
2040
|
)
|
|
2196
2041
|
] })
|
package/package.json
CHANGED
|
@@ -1,43 +1,43 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@campfire-interactive/volume-intelligence-ui",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Shared Volume Intelligence UI components (volume import) for the Campfire Suite — embedded by the VI webapp and by consuming apps (OMSF) so there is one import surface, not two that drift.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "dist/index.js",
|
|
7
|
-
"types": "dist/index.d.ts",
|
|
8
|
-
"exports": {
|
|
9
|
-
".": {
|
|
10
|
-
"types": "./dist/index.d.ts",
|
|
11
|
-
"import": "./dist/index.js"
|
|
12
|
-
}
|
|
13
|
-
},
|
|
14
|
-
"files": [
|
|
15
|
-
"dist"
|
|
16
|
-
],
|
|
17
|
-
"scripts": {
|
|
18
|
-
"build": "tsup",
|
|
19
|
-
"dev": "tsup --watch"
|
|
20
|
-
},
|
|
21
|
-
"peerDependencies": {
|
|
22
|
-
"react": "^18.0.0 || ^19.0.0",
|
|
23
|
-
"react-dom": "^18.0.0 || ^19.0.0"
|
|
24
|
-
},
|
|
25
|
-
"dependencies": {
|
|
26
|
-
"@campfire-interactive/design-tokens": "*",
|
|
27
|
-
"lucide-react": "^0.487.0"
|
|
28
|
-
},
|
|
29
|
-
"devDependencies": {
|
|
30
|
-
"@types/react": "^18.2.0",
|
|
31
|
-
"react": "^18.2.0",
|
|
32
|
-
"react-dom": "^18.2.0",
|
|
33
|
-
"tsup": "^8.0.0",
|
|
34
|
-
"typescript": "^5.3.3"
|
|
35
|
-
},
|
|
36
|
-
"publishConfig": {
|
|
37
|
-
"registry": "https://registry.npmjs.org",
|
|
38
|
-
"access": "public"
|
|
39
|
-
},
|
|
40
|
-
"engines": {
|
|
41
|
-
"node": ">=18.0.0"
|
|
42
|
-
}
|
|
43
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@campfire-interactive/volume-intelligence-ui",
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "Shared Volume Intelligence UI components (volume import) for the Campfire Suite — embedded by the VI webapp and by consuming apps (OMSF) so there is one import surface, not two that drift.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsup",
|
|
19
|
+
"dev": "tsup --watch"
|
|
20
|
+
},
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
23
|
+
"react-dom": "^18.0.0 || ^19.0.0"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@campfire-interactive/design-tokens": "*",
|
|
27
|
+
"lucide-react": "^0.487.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/react": "^18.2.0",
|
|
31
|
+
"react": "^18.2.0",
|
|
32
|
+
"react-dom": "^18.2.0",
|
|
33
|
+
"tsup": "^8.0.0",
|
|
34
|
+
"typescript": "^5.3.3"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"registry": "https://registry.npmjs.org",
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=18.0.0"
|
|
42
|
+
}
|
|
43
|
+
}
|