@campfire-interactive/volume-intelligence-ui 0.4.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 +50 -9
- package/dist/index.js +289 -402
- 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;
|
|
@@ -258,6 +258,17 @@ interface ClearSourceDataResult {
|
|
|
258
258
|
criteria: number;
|
|
259
259
|
cleared: true;
|
|
260
260
|
}
|
|
261
|
+
/** Whether a source satisfies every required identity field. The dual-
|
|
262
|
+
* satisfaction rule (explicit automotive_field mapping OR the covering
|
|
263
|
+
* hierarchy level) is applied server-side, so the UI just renders this. */
|
|
264
|
+
interface RequiredMappingStatus {
|
|
265
|
+
satisfied: boolean;
|
|
266
|
+
/** Required fields with neither an explicit mapping nor a covering level. */
|
|
267
|
+
missing: string[];
|
|
268
|
+
/** True only for wide base sources — the scope of the ingest/activation gate;
|
|
269
|
+
* tall sources carry a minimal identity set and overlays inherit from base. */
|
|
270
|
+
enforced: boolean;
|
|
271
|
+
}
|
|
261
272
|
/**
|
|
262
273
|
* Everything the source-management components need from the host. VI-webapp
|
|
263
274
|
* implements these against its `adminApi`; OMSF implements them against proxy
|
|
@@ -296,6 +307,20 @@ interface SourceAdminTransport {
|
|
|
296
307
|
setTenantSource(sourceId: string): Promise<TenantVolumeConfig>;
|
|
297
308
|
listUploads(params?: UploadListParams): Promise<UploadListResponse>;
|
|
298
309
|
clearSourceData(sourceId: string, confirm: string): Promise<ClearSourceDataResult>;
|
|
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;
|
|
299
324
|
}
|
|
300
325
|
|
|
301
326
|
interface VolumeFilesTransport {
|
|
@@ -356,6 +381,22 @@ interface VolumeSourcesListProps {
|
|
|
356
381
|
*/
|
|
357
382
|
declare function VolumeSourcesList({ transport, onOpenSource, renderActiveSourceExtras, className }: VolumeSourcesListProps): react.JSX.Element;
|
|
358
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
|
+
*/
|
|
359
400
|
interface AutomotiveSlot {
|
|
360
401
|
key: string;
|
|
361
402
|
label: string;
|
|
@@ -365,11 +406,10 @@ interface AutomotiveSlot {
|
|
|
365
406
|
}
|
|
366
407
|
interface UnifiedSegmentMapperProps {
|
|
367
408
|
segments: VolumeSourceSegment[];
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
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[];
|
|
373
413
|
onSave: (hierarchy: Array<{
|
|
374
414
|
targetKey: string;
|
|
375
415
|
segmentId: string;
|
|
@@ -377,12 +417,13 @@ interface UnifiedSegmentMapperProps {
|
|
|
377
417
|
targetKey: string;
|
|
378
418
|
segmentId: string;
|
|
379
419
|
transformRule?: string | null;
|
|
420
|
+
}>, dimensions: Array<{
|
|
421
|
+
targetKey: string;
|
|
422
|
+
segmentId: string;
|
|
380
423
|
}>) => Promise<void>;
|
|
381
424
|
saving: boolean;
|
|
382
|
-
hierarchyMode?: 'tree' | 'sparse';
|
|
383
|
-
baseHierarchyHint?: Partial<Record<number, string>>;
|
|
384
425
|
}
|
|
385
|
-
declare function UnifiedSegmentMapper({ segments,
|
|
426
|
+
declare function UnifiedSegmentMapper({ segments, mappableDimensions, initialMappings, onSave, saving, }: UnifiedSegmentMapperProps): react.JSX.Element;
|
|
386
427
|
|
|
387
428
|
interface VolumeSourceDetailProps {
|
|
388
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
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
for (const slot of automotiveSlots) {
|
|
1058
|
-
const existing = automotiveInitial.find((m) => m.targetKey === slot.key);
|
|
1059
|
-
initAuto[slot.key] = {
|
|
1060
|
-
segmentId: existing?.segmentId ?? null,
|
|
1061
|
-
transformRule: existing?.transformRule ?? slot.defaultTransform ?? null
|
|
1062
|
-
};
|
|
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);
|
|
1063
1115
|
}
|
|
1064
|
-
|
|
1065
|
-
|
|
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,7 +1597,19 @@ 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 [
|
|
1600
|
+
const [requiredStatus, setRequiredStatus] = useState7(null);
|
|
1601
|
+
const [mappableDimensions, setMappableDimensions] = useState7([]);
|
|
1602
|
+
async function loadRequiredStatus() {
|
|
1603
|
+
if (!id || !transport.getRequiredMappingStatus) {
|
|
1604
|
+
setRequiredStatus(null);
|
|
1605
|
+
return;
|
|
1606
|
+
}
|
|
1607
|
+
try {
|
|
1608
|
+
setRequiredStatus(await transport.getRequiredMappingStatus(id));
|
|
1609
|
+
} catch {
|
|
1610
|
+
setRequiredStatus(null);
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1734
1613
|
async function load() {
|
|
1735
1614
|
if (!id) return;
|
|
1736
1615
|
try {
|
|
@@ -1750,32 +1629,11 @@ function VolumeSourceDetail({ transport, sourceId, onOpenSource, onBack, classNa
|
|
|
1750
1629
|
setHasData(uploads.items.some((u) => u.status === "done" || u.status === "done_with_errors"));
|
|
1751
1630
|
setMappingKeys(keys);
|
|
1752
1631
|
setMappingRows(mapping.items);
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
if (tenantConfig?.sourceId && tenantConfig.sourceId !== id) {
|
|
1757
|
-
const [baseSrc, baseMapping] = await Promise.all([
|
|
1758
|
-
transport.getSource(tenantConfig.sourceId),
|
|
1759
|
-
transport.listSegmentMapping(tenantConfig.sourceId)
|
|
1760
|
-
]);
|
|
1761
|
-
const baseSegLabel = new Map(baseSrc.segments.map((s) => [s.id, s.label]));
|
|
1762
|
-
const hint = {};
|
|
1763
|
-
for (const row of baseMapping.items) {
|
|
1764
|
-
if (row.targetCategory !== "hierarchy_level") continue;
|
|
1765
|
-
const m = /^level(\d)$/.exec(row.targetKey);
|
|
1766
|
-
if (!m) continue;
|
|
1767
|
-
const label = baseSegLabel.get(row.segmentId);
|
|
1768
|
-
if (label) hint[parseInt(m[1], 10)] = label;
|
|
1769
|
-
}
|
|
1770
|
-
setBaseHierarchyHint(hint);
|
|
1771
|
-
} else {
|
|
1772
|
-
setBaseHierarchyHint({});
|
|
1773
|
-
}
|
|
1774
|
-
} catch {
|
|
1775
|
-
setBaseHierarchyHint({});
|
|
1776
|
-
}
|
|
1632
|
+
void loadRequiredStatus();
|
|
1633
|
+
if (transport.getMappableDimensions) {
|
|
1634
|
+
transport.getMappableDimensions().then(setMappableDimensions).catch(() => setMappableDimensions([]));
|
|
1777
1635
|
} else {
|
|
1778
|
-
|
|
1636
|
+
setMappableDimensions([]);
|
|
1779
1637
|
}
|
|
1780
1638
|
} catch (e) {
|
|
1781
1639
|
setError(e instanceof Error ? e.message : "Failed to load source");
|
|
@@ -1809,14 +1667,20 @@ function VolumeSourceDetail({ transport, sourceId, onOpenSource, onBack, classNa
|
|
|
1809
1667
|
setError(e instanceof Error ? e.message : "Delete failed");
|
|
1810
1668
|
}
|
|
1811
1669
|
}
|
|
1812
|
-
async function handleSaveMapping(hierarchy, automotive) {
|
|
1670
|
+
async function handleSaveMapping(hierarchy, automotive, dimensions) {
|
|
1813
1671
|
if (!id) return;
|
|
1814
1672
|
setSavingMapping(true);
|
|
1815
1673
|
setError(null);
|
|
1816
1674
|
try {
|
|
1817
1675
|
const hResp = await transport.replaceSegmentMappingCategory(id, "hierarchy_level", hierarchy);
|
|
1818
1676
|
const aResp = await transport.replaceSegmentMappingCategory(id, "automotive_field", automotive);
|
|
1819
|
-
|
|
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);
|
|
1683
|
+
void loadRequiredStatus();
|
|
1820
1684
|
} catch (e) {
|
|
1821
1685
|
setError(e instanceof Error ? e.message : "Failed to save mapping");
|
|
1822
1686
|
} finally {
|
|
@@ -2136,19 +2000,42 @@ function VolumeSourceDetail({ transport, sourceId, onOpenSource, onBack, classNa
|
|
|
2136
2000
|
/* @__PURE__ */ jsx8("h2", { className: "cfi-vi-h2", children: "Segment Mapping" }),
|
|
2137
2001
|
isOverlay && /* @__PURE__ */ jsx8("span", { className: "cfi-vi-muted", style: { fontSize: "var(--cfi-font-size-xs, 12px)" }, children: "\xB7 Map at least the levels your base source uses for the overlay merge to land." })
|
|
2138
2002
|
] }),
|
|
2003
|
+
requiredStatus?.enforced && (requiredStatus.satisfied ? /* @__PURE__ */ jsxs8(
|
|
2004
|
+
"div",
|
|
2005
|
+
{
|
|
2006
|
+
className: "cfi-vi-row",
|
|
2007
|
+
style: { gap: 6, color: "#15803d", fontSize: "var(--cfi-font-size-sm, 13px)", margin: "4px 0 8px" },
|
|
2008
|
+
children: [
|
|
2009
|
+
/* @__PURE__ */ jsx8("span", { "aria-hidden": true, children: "\u2713" }),
|
|
2010
|
+
" All required mappings are set \u2014 this source can be activated and ingested."
|
|
2011
|
+
]
|
|
2012
|
+
}
|
|
2013
|
+
) : /* @__PURE__ */ jsxs8(
|
|
2014
|
+
"div",
|
|
2015
|
+
{
|
|
2016
|
+
className: "cfi-vi-lock",
|
|
2017
|
+
style: { display: "flex", alignItems: "flex-start", gap: 8, margin: "4px 0 8px" },
|
|
2018
|
+
children: [
|
|
2019
|
+
/* @__PURE__ */ jsx8(AlertTriangle3, { size: 14, "aria-hidden": true, style: { marginTop: 2, flexShrink: 0 } }),
|
|
2020
|
+
/* @__PURE__ */ jsxs8("span", { children: [
|
|
2021
|
+
"Missing required mapping",
|
|
2022
|
+
requiredStatus.missing.length === 1 ? "" : "s",
|
|
2023
|
+
":",
|
|
2024
|
+
" ",
|
|
2025
|
+
requiredStatus.missing.map((m) => /* @__PURE__ */ jsx8("span", { className: "cfi-vi-mono", style: { marginRight: 6 }, children: m }, m)),
|
|
2026
|
+
"\u2014 map these (directly, or via the covering hierarchy level) before this source can be activated or ingest."
|
|
2027
|
+
] })
|
|
2028
|
+
]
|
|
2029
|
+
}
|
|
2030
|
+
)),
|
|
2139
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(
|
|
2140
2032
|
UnifiedSegmentMapper,
|
|
2141
2033
|
{
|
|
2142
|
-
segments: source.segments
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
automotiveSlots: isOverlay ? [] : AUTOMOTIVE_SLOTS,
|
|
2146
|
-
transformRules: mappingKeys?.transformRules ?? [],
|
|
2147
|
-
transformRuleLabels: AUTOMOTIVE_TRANSFORM_LABELS,
|
|
2034
|
+
segments: source.segments,
|
|
2035
|
+
mappableDimensions,
|
|
2036
|
+
initialMappings: mappingRows,
|
|
2148
2037
|
onSave: handleSaveMapping,
|
|
2149
|
-
saving: savingMapping
|
|
2150
|
-
hierarchyMode: isOverlay ? "sparse" : "tree",
|
|
2151
|
-
baseHierarchyHint: isOverlay ? baseHierarchyHint : void 0
|
|
2038
|
+
saving: savingMapping
|
|
2152
2039
|
}
|
|
2153
2040
|
)
|
|
2154
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
|
+
}
|