d10b1d263f
A selector is a nomenclature whose value decides which other nomenclatures appear beneath it: picking "Ship" reveals the ship fields, "Warehouse" reveals the warehouse ones. Previously every vehicle nomenclature rendered at once, so the operator had to know which were relevant and a vehicle could silently carry both a ship and a warehouse. Visibility is one rule - a nomenclature is hidden if some selector option reveals it and that selector's value is not the revealing option - kept in nomenclatures/visibility.py and mirrored in services/nomenclatureVisibility.js. A nomenclature no selector references always shows, so installations without selectors are unchanged. The serializer, not the UI, enforces the invariant: a value for a hidden nomenclature is rejected, as is a selector value that matches no option. The frontend clearing values on switch and pruning hidden keys before save is convenience on top of that. Selector values join lookups in the quick-fill combination projection, so applying a saved combination sets the source and its revealed fields in one click rather than collapsing "Ship + cargo" and "Warehouse + cargo" into one indistinguishable row. Configuration rejects nesting (a selector revealing another selector) and double-reveal (one nomenclature revealed by two selectors, leaving its visibility ambiguous). The admin UI filters both out of the reveal list as well, so an invalid configuration cannot be built in the first place. Note: introducing a selector over existing data hides values already entered - vehicles predating the selector have no selector key, so anything it reveals becomes invisible until a source is chosen. The values remain stored. Worth a data check before configuring the first selector. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
186 lines
6.5 KiB
React
186 lines
6.5 KiB
React
import { useState, useMemo } from 'react';
|
|
import { useNomenclatureData } from '../../contexts/NomenclatureDataContext';
|
|
import NomenclatureManagementOverlay from './NomenclatureManagementOverlay';
|
|
import './NomenclatureDropdown.css';
|
|
|
|
export default function NomenclatureDropdown({
|
|
nomenclatureCode,
|
|
value,
|
|
onChange,
|
|
label,
|
|
disabled = false,
|
|
}) {
|
|
const { definitions, getActiveEntries, isLoading } = useNomenclatureData();
|
|
const [showOverlay, setShowOverlay] = useState(false);
|
|
|
|
const nomenclature = definitions[nomenclatureCode];
|
|
const activeEntries = useMemo(
|
|
() => getActiveEntries(nomenclatureCode),
|
|
[getActiveEntries, nomenclatureCode]
|
|
);
|
|
|
|
if (!nomenclature) {
|
|
if (isLoading) {
|
|
return (
|
|
<div className="nui-dropdown">
|
|
{label && <label className="nui-dropdown-label">{label}</label>}
|
|
<select disabled className="nui-dropdown-select">
|
|
<option>Loading...</option>
|
|
</select>
|
|
</div>
|
|
);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
const displayLabel = label || nomenclature.name;
|
|
|
|
// SELECTOR type: choices come from selector_options, value is the option string.
|
|
// No manage button - options are configured in the admin screen, not while weighing.
|
|
if (nomenclature.kind === 'selector') {
|
|
const options = nomenclature.selector_options || [];
|
|
const isUnknown = value && !options.some(o => o.value === value);
|
|
|
|
return (
|
|
<div className="nui-dropdown">
|
|
<label className="nui-dropdown-label">{displayLabel}</label>
|
|
<select
|
|
className="nui-dropdown-select"
|
|
value={value || ''}
|
|
onChange={(e) => onChange(e.target.value || undefined)}
|
|
disabled={disabled}
|
|
>
|
|
<option value="">-- Select --</option>
|
|
{options.map(opt => (
|
|
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
|
))}
|
|
{isUnknown && (
|
|
<option value={value}>{value} (unknown)</option>
|
|
)}
|
|
</select>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// LOOKUP type: select dropdown + manage button
|
|
if (nomenclature.kind === 'lookup') {
|
|
const handleSelectChange = (e) => {
|
|
const val = e.target.value;
|
|
onChange(val ? parseInt(val, 10) : undefined);
|
|
};
|
|
|
|
const handleOverlaySelect = (entryId) => {
|
|
onChange(entryId);
|
|
setShowOverlay(false);
|
|
};
|
|
|
|
return (
|
|
<div className="nui-dropdown">
|
|
<label className="nui-dropdown-label">{displayLabel}</label>
|
|
<div className="nui-dropdown-row">
|
|
<select
|
|
className="nui-dropdown-select"
|
|
value={value || ''}
|
|
onChange={handleSelectChange}
|
|
disabled={disabled}
|
|
>
|
|
<option value="">-- Select --</option>
|
|
{activeEntries.map(entry => (
|
|
<option key={entry.id} value={entry.id}>
|
|
{entry.display_value}
|
|
</option>
|
|
))}
|
|
</select>
|
|
<button
|
|
className="nui-dropdown-manage-btn"
|
|
onClick={() => setShowOverlay(true)}
|
|
disabled={disabled}
|
|
type="button"
|
|
title={`Manage ${displayLabel}`}
|
|
>
|
|
…
|
|
</button>
|
|
</div>
|
|
|
|
<NomenclatureManagementOverlay
|
|
nomenclatureCode={nomenclatureCode}
|
|
isOpen={showOverlay}
|
|
onClose={() => setShowOverlay(false)}
|
|
mode="select"
|
|
initialSelection={value || null}
|
|
onSelect={handleOverlaySelect}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// FIELD type: render appropriate input
|
|
const fieldDef = nomenclature.fields?.[0];
|
|
if (!fieldDef) return null;
|
|
|
|
if (fieldDef.field_type === 'bool') {
|
|
return (
|
|
<div className="nui-dropdown">
|
|
<label className="nui-dropdown-label nui-dropdown-label--checkbox">
|
|
<input
|
|
type="checkbox"
|
|
checked={value || false}
|
|
onChange={(e) => onChange(e.target.checked)}
|
|
disabled={disabled}
|
|
className="nui-dropdown-checkbox"
|
|
/>
|
|
{displayLabel}
|
|
</label>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (fieldDef.field_type === 'number') {
|
|
return (
|
|
<div className="nui-dropdown">
|
|
<label className="nui-dropdown-label">{displayLabel}</label>
|
|
<input
|
|
type="number"
|
|
className="nui-dropdown-input"
|
|
value={value ?? ''}
|
|
onChange={(e) => onChange(e.target.value === '' ? undefined : Number(e.target.value))}
|
|
disabled={disabled}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (fieldDef.field_type === 'choice' && fieldDef.choices) {
|
|
return (
|
|
<div className="nui-dropdown">
|
|
<label className="nui-dropdown-label">{displayLabel}</label>
|
|
<select
|
|
className="nui-dropdown-select"
|
|
value={value || ''}
|
|
onChange={(e) => onChange(e.target.value || undefined)}
|
|
disabled={disabled}
|
|
>
|
|
<option value="">-- Select --</option>
|
|
{fieldDef.choices.map(c => (
|
|
<option key={c} value={c}>{c}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Default: text input
|
|
return (
|
|
<div className="nui-dropdown">
|
|
<label className="nui-dropdown-label">{displayLabel}</label>
|
|
<input
|
|
type="text"
|
|
className="nui-dropdown-input"
|
|
value={value || ''}
|
|
onChange={(e) => onChange(e.target.value || undefined)}
|
|
disabled={disabled}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|