Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions frontend/source/class/agrammon/module/input/NavBar.js
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,14 @@ qx.Class.define('agrammon.module.input.NavBar', {
var olen = rec.options.length;
metaData.options = rec.options;
metaData.optionsLang = rec.optionsLang;
// Cross-version enum alias map ({alias: canonical}) —
// declared via `accepts =` in the .nhd. Used by
// NavFolder.setData (load), the Replace cell renderer
// (orange highlight + canonical label), and the
// PropTable replaceMap.
if (rec.enumAliases) {
metaData.enumAliases = rec.enumAliases;
}
}
else {
var err = 'This should not happen: unknown variable type=' + rec.type + ' for variable ' + rec.variable;
Expand Down
82 changes: 72 additions & 10 deletions frontend/source/class/agrammon/module/input/NavFolder.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,14 @@ qx.Class.define('agrammon.module.input.NavFolder', {
__propData: null,
__childrenHash: null,
__instanceOrder: null,
// Tracks whether any input under this folder currently holds a value
// that was display-mapped from a foreign-version enum alias. Drives
// the orange dot/circle in the navbar. Session-only; never persisted.
__mapped: false,

isMapped: function() {
return this.__mapped;
},

destruct : function() {
this.__propData = null;
Expand Down Expand Up @@ -269,6 +277,20 @@ qx.Class.define('agrammon.module.input.NavFolder', {
}
}

// Aggregate mapped state from children: any descendant input
// holding a foreign-version alias makes this folder orange-when-
// otherwise-green. Missing data still wins (red), since orange
// is informational and must not mask incomplete inputs.
var mapped = false;
for (key in this.__childrenHash) {
if (typeof this.__childrenHash[key].isMapped === 'function'
&& this.__childrenHash[key].isMapped()) {
mapped = true;
break;
}
}
this.__mapped = mapped;

// no direct data
if (this.isPlain()) {
if (complete == undefined) {
Expand All @@ -277,22 +299,28 @@ qx.Class.define('agrammon.module.input.NavFolder', {
complete = true;
}
}
else if (complete) {
this.setIcon('agrammon/green-dot.png');
else if (!complete) {
this.setIcon('agrammon/red-dot.png');
}
else if (mapped) {
this.setIcon('agrammon/orange-dot.png');
}
else {
this.setIcon('agrammon/red-dot.png');
this.setIcon('agrammon/green-dot.png');
}
}
if (this.canInstance()) {
if (complete == undefined) {
this.setIcon('agrammon/empty-circle.png');
}
else if (complete) {
this.setIcon('agrammon/green-circle.png');
else if (!complete) {
this.setIcon('agrammon/red-circle.png');
}
else if (mapped) {
this.setIcon('agrammon/orange-circle.png');
}
else {
this.setIcon('agrammon/red-circle.png');
this.setIcon('agrammon/green-circle.png');
}
}
if (this.isRoot()) {
Expand Down Expand Up @@ -336,6 +364,7 @@ qx.Class.define('agrammon.module.input.NavFolder', {

// not empty
var complete = true;
var mapped = false;
for (let i=0; i<len; i++) {
let varName = this.__propData[i].getName();
let metaData = {};
Expand All @@ -347,7 +376,11 @@ qx.Class.define('agrammon.module.input.NavFolder', {
(value === '*** Select ***' && defaultValue === '*** Select ***')
) { // incomplete
complete = false;
break; // one false is enough
// Don't break here: we still want to detect any mapped
// values further down the list so a single missing
// input doesn't suppress the orange signal once it
// gets filled in.
continue;
}
else if (value === 'branched') {
metaData = this.__propData[i].getMetaData();
Expand All @@ -365,14 +398,32 @@ qx.Class.define('agrammon.module.input.NavFolder', {
}
}
} // value === branched
else {
// Mapped: stored value is a cross-version alias for
// one of the local enum keys. enumAliases is populated
// by the backend's Input.as-hash when `accepts =` is
// declared in the .nhd. Surfaces as orange; does not
// block calculation.
metaData = this.__propData[i].getMetaData();
if ( metaData
&& metaData.enumAliases
&& metaData.enumAliases.hasOwnProperty(value)
) {
mapped = true;
}
}
}
}
this.__mapped = mapped;

if (complete) {
this.setIcon('agrammon/green-dot.png');
if (!complete) {
this.setIcon('agrammon/red-dot.png');
}
else if (mapped) {
this.setIcon('agrammon/orange-dot.png');
}
else {
this.setIcon('agrammon/red-dot.png');
this.setIcon('agrammon/green-dot.png');
}
return complete;
}, // isComplete
Expand Down Expand Up @@ -438,6 +489,17 @@ qx.Class.define('agrammon.module.input.NavFolder', {
break;
}
}
// Cross-version enum alias: declared via `accepts =`
// in the .nhd. Accept the value, keep it as-is in
// storage (so reopening in the source version still
// sees its own value); the cell renderer maps it
// to the canonical label and paints it orange.
if ( ! found
&& metaData.enumAliases
&& metaData.enumAliases.hasOwnProperty(value)
) {
found = true;
}
if (found) {
this.__propData[i].setValue(value);
}
Expand Down
37 changes: 37 additions & 0 deletions frontend/source/class/agrammon/module/input/PropTable.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,36 @@ qx.Class.define('agrammon.module.input.PropTable', {
});
renderer.setReplaceMap(replaceMap);
renderer.addReversedReplaceMap();
// Map foreign-version enum aliases (declared as
// `accepts =` in the .nhd) to the canonical key's
// localized label so an aliased cell renders with the
// canonical label (and the Replace renderer paints
// it orange via the enumAliases check).
if (metaData.enumAliases) {
var locale = qx.locale.Manager.getInstance().getLocale();
locale = locale.replace(/_.+/,'');
// Build canonicalKey → label index from the option
// rows (rows are [label, '', key]).
var keyToLabel = {};
var j=0;
metaData['options'].forEach(function(row) {
if (row instanceof Array) {
keyToLabel[row[2]] = metaData.optionsLang[j][locale];
j++;
}
});
// Adding entries to the renderer's existing map (which
// already contains both forward and reversed mappings
// after addReversedReplaceMap above).
var fullMap = renderer.getReplaceMap() || replaceMap;
for (var alias in metaData.enumAliases) {
var canonical = metaData.enumAliases[alias];
if (keyToLabel[canonical] !== undefined) {
fullMap[alias] = keyToLabel[canonical];
}
}
renderer.setReplaceMap(fullMap);
}
return renderer;
}
}
Expand Down Expand Up @@ -101,6 +131,13 @@ qx.Class.define('agrammon.module.input.PropTable', {
break;
case "optionsLang":
break;
case "enumAliases":
// Consumed by the cell renderer (Replace.js) and by
// setData/isComplete in NavFolder.js. The SelectBox
// editor only shows local options, so nothing to do
// here — but the key must be acknowledged or the
// default branch alerts.
break;
case "type":
switch ( metaData.type ) {
case "checkbox":
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,18 @@ qx.Class.define('agrammon.ui.table.cellrenderer.input.Replace', {
}
}
default:
color = (cellInfo.row % 2 == 1 ? "#c5e1af" : "#dff1d1");
// Orange when the stored value is a foreign-version enum alias
// mapped onto a local key (declared via `accepts =` in the .nhd).
// Informational only — does not block calculation.
if ( metaData
&& metaData.enumAliases
&& metaData.enumAliases.hasOwnProperty(cellInfo.value)
) {
color = (cellInfo.row % 2 == 1 ? "#f9d8a4" : "#fbeacb");
}
else {
color = (cellInfo.row % 2 == 1 ? "#c5e1af" : "#dff1d1");
}
break;
}
return this.base(arguments, cellInfo) + "background-color:" + color + ";";
Expand Down
Binary file added frontend/source/resource/agrammon/orange-circle.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/source/resource/agrammon/orange-dot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion lib/Agrammon/Inputs.rakumod
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ role Agrammon::Inputs::Storage {
}
orwith $input.compiled-default-formula -> &default {
%values{$input.name} //= default(Agrammon::Environment.new(
input => %values,
input => $module.canonicalize-input-hash(%values),
technical => $module.technical-hash,
technical-override => %technical{$taxonomy}))
}
Expand Down
12 changes: 10 additions & 2 deletions lib/Agrammon/LanguageParser.rakumod
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,16 @@ sub parse-lang-values(Str $value, Str $context --> Hash) is export {
warn "Failed to parse language value for $context: ol=\>$ol\<";
next;
}
$o ~~ s:g/_/ /;
%opt-lang{$l} = $o;
# `accepts` is a comma-separated list of foreign enum keys this
# option accepts as aliases (cross-version migration). Preserve
# underscores and split on commas; do not treat as language text.
if $l eq 'accepts' {
%opt-lang{$l} = $o.split(/\s* ',' \s*/).grep(*.chars).list;
}
else {
$o ~~ s:g/_/ /;
%opt-lang{$l} = $o;
}
}
%opt-lang
}
2 changes: 1 addition & 1 deletion lib/Agrammon/Model.rakumod
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ class Agrammon::Model {
my $*AGRAMMON-TAXONOMY = my $tax = $!module.taxonomy;
my %*AGRAMMON-GUI = %(:de(@gui[1]), :fr(@gui[2]), :en(@gui[3])) if @gui;
my $env = Agrammon::Environment.new(
input => $input.input-hash-for($tax),
input => $!module.canonicalize-input-hash($input.input-hash-for($tax)),
technical => $!module.technical-hash,
technical-override => %technical{$tax},
output => $outputs
Expand Down
37 changes: 35 additions & 2 deletions lib/Agrammon/Model/Input.rakumod
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class Agrammon::Model::Input {
has Str @.models;
has @!enum-order;
has %!enum-lookup;
has %!enum-aliases;
has Int $.order;
has Bool $!hidden = False;
has Bool $!distribute = False;
Expand All @@ -46,7 +47,20 @@ class Agrammon::Model::Input {
$!distribute = .lc eq 'true';
}
if @enum {
@!enum-order = @enum.map({ .key => parse-lang-values(.value, "input $!name") });
@!enum-order = @enum.map({
my $key = .key;
my %lang-values = parse-lang-values(.value, "input $!name");
with %lang-values<accepts>:delete -> $accepts {
for @$accepts -> $alias {
if %!enum-aliases{$alias}:exists {
warn "Duplicate enum alias '$alias' in input $!name " ~
"(already maps to '%!enum-aliases{$alias}', now also '$key')";
}
%!enum-aliases{$alias} = $key;
}
}
$key => %lang-values
});
%!enum-lookup = @!enum-order;
}
with $filter {
Expand All @@ -63,7 +77,25 @@ class Agrammon::Model::Input {
}

method is-valid-enum-value($value) {
%!enum-lookup{$value}:exists
%!enum-lookup{$value}:exists or %!enum-aliases{$value}:exists
}

#| Returns the canonical (locally declared) enum key for $value.
#| If $value is already a local key, returns it unchanged. If it is
#| a declared alias, returns the local key it maps to. Otherwise Nil.
method canonical-enum-value($value) {
return $value if %!enum-lookup{$value}:exists;
return %!enum-aliases{$value} // Nil;
}

#| True iff $value is a declared alias (i.e. comes from another model
#| version) and is being mapped onto a local enum key.
method is-mapped-enum-value($value --> Bool) {
%!enum-aliases{$value}:exists
}

method enum-aliases(--> Hash) {
%!enum-aliases
}

method is-distribute(--> Bool) {
Expand Down Expand Up @@ -109,6 +141,7 @@ class Agrammon::Model::Input {
hasFormula => $.default-formula.defined,
)),
:enum(%!enum-lookup),
:enumAliases(%!enum-aliases),
:$!filter,
:%!help,
:%!labels,
Expand Down
18 changes: 18 additions & 0 deletions lib/Agrammon/Model/Module.rakumod
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,22 @@ class Agrammon::Model::Module {
method set-instance-root(Str $!instance-root) {}

method set-gui-root(Agrammon::Model::Module $!gui-root-module) {}

#| Returns a copy of $input-hash with enum-typed input values canonicalized:
#| values that are declared `accepts =` aliases (e.g. cross-version
#| migration) are rewritten to the local enum key so that formulas only
#| ever see the canonical local values. Non-enum values pass through.
method canonicalize-input-hash(%input-hash --> Hash) {
my %out = %input-hash;
for @!input -> Agrammon::Model::Input $input {
next unless $input.type eq 'enum';
my $name = $input.name;
with %out{$name} -> $value {
if $input.is-mapped-enum-value($value) {
%out{$name} = $input.canonical-enum-value($value);
}
}
}
%out
}
}
6 changes: 6 additions & 0 deletions runWebDev.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,10 @@ if ! podman container exists agrammon-dev-db 2>/dev/null; then
fi

export PERL5LIB=Inline/perl5
# SOURCE_MODE=1 makes the Cro static-content routes serve the qooxdoo
# source target (frontend/compiled/source/) instead of the production
# build target (public/). Frontend edits then pick up after a single
# `npx qx compile` (or live via `npx qx compile --watch`) without
# needing a full minified rebuild.
export SOURCE_MODE=1
exec raku -Ilib bin/agrammon.raku --cfg-file=dev/agrammon.dev.yaml web version6/End.nhd
Loading
Loading