diff --git a/app/guid-node/metadata/template.hbs b/app/guid-node/metadata/template.hbs index 1d9f3eaf4..515356713 100644 --- a/app/guid-node/metadata/template.hbs +++ b/app/guid-node/metadata/template.hbs @@ -91,11 +91,11 @@ @changed={{action this.schemaChanged}} >
+ + {{t 'osf-components.draft-registration-card.workflow_submitted'}} + +
+ {{/if}} {{#if this.wekoItemId}}- {{t 'osf-components.draft-registration-card.weko_id' weko_id=this.wekoItemId}} + {{t (concat 'osf-components.draft-registration-card.weko_id.' this.wekoLabelKey) weko_id=this.wekoItemId}}
{{/if}}@@ -35,7 +42,7 @@
{{t 'osf-components.draft-registration-card.form_type'}} - {{this.draftRegistration.registrationSchema.name}} + {{this.draftRegistration.registrationSchema.localizedName}}
{{t 'osf-components.draft-registration-card.started'}} diff --git a/lib/osf-components/addon/components/node-card/template.hbs b/lib/osf-components/addon/components/node-card/template.hbs index eef68cfe6..27025efa4 100644 --- a/lib/osf-components/addon/components/node-card/template.hbs +++ b/lib/osf-components/addon/components/node-card/template.hbs @@ -113,7 +113,7 @@
+ local-class='DisplayText {{if this.displayTextOverride 'SubLabel'}}'> + {{~#if (eq this.itemMarker 'circle')~}}{{this.circleMarker}} {{~/if~}} {{~this.localizedDisplayText~}} {{~#if (and @isRequired (not @readonly))~}} * {{~/if~}}
+{{~#each this.itemTags as |tag|~}} + + {{~tag.localizedText~}} + {{~#if tag.info~}} +{{this.schemaBlock.helpText}}
+{{this.localizedHelpText}}
- {{t 'registries.drafts.draft.weko_id' weko_id=this.wekoItemId htmlSafe=true}} + {{t (concat 'registries.drafts.draft.weko_id.' this.wekoLabelKey) weko_id=this.wekoItemId htmlSafe=true}}
{{/if}} { + test('truthy field returns true', assert => { + assert.ok(evaluateExpression('name', { name: 'Alice' })); + }); + + test('missing field returns false', assert => { + assert.notOk(evaluateExpression('name', {})); + }); + + test('null field returns false', assert => { + assert.notOk(evaluateExpression('name', { name: null })); + }); + + test('empty string field returns false', assert => { + assert.notOk(evaluateExpression('name', { name: '' })); + }); + + test('true literal', assert => { + assert.ok(evaluateExpression('true', {})); + }); + + test('false literal', assert => { + assert.notOk(evaluateExpression('false', {})); + }); + + test('field prefixed with true is not confused', assert => { + assert.ok(evaluateExpression('trueness', { trueness: 'yes' })); + }); + + test('string equality', assert => { + assert.ok(evaluateExpression("status == 'active'", { status: 'active' })); + }); + + test('string inequality', assert => { + assert.ok(evaluateExpression("status != 'active'", { status: 'inactive' })); + }); + + test('OR left true', assert => { + assert.ok(evaluateExpression('a || b', { a: 'x', b: '' })); + }); + + test('OR right true', assert => { + assert.ok(evaluateExpression('a || b', { a: '', b: 'x' })); + }); + + test('OR both false', assert => { + assert.notOk(evaluateExpression('a || b', { a: '', b: '' })); + }); + + test('AND both true', assert => { + assert.ok(evaluateExpression('a && b', { a: 'x', b: 'y' })); + }); + + test('AND left false', assert => { + assert.notOk(evaluateExpression('a && b', { a: '', b: 'y' })); + }); + + test('NOT truthy', assert => { + assert.notOk(evaluateExpression('!a', { a: 'x' })); + }); + + test('NOT falsy', assert => { + assert.ok(evaluateExpression('!a', { a: '' })); + }); + + test('double NOT', assert => { + assert.ok(evaluateExpression('!!a', { a: 'x' })); + }); + + test('AND binds tighter than OR', assert => { + // false || (true && true) => true + assert.ok(evaluateExpression('a || b && c', { a: '', b: 'x', c: 'y' })); + }); + + test('NOT binds tighter than AND', assert => { + // (!false) && true => true + assert.ok(evaluateExpression('!a && b', { a: '', b: 'x' })); + }); + + test('parentheses override precedence', assert => { + // !(false && true) => true + assert.ok(evaluateExpression('!(a && b)', { a: '', b: 'x' })); + }); + + test('chained OR parses all operands', assert => { + assert.ok(evaluateExpression('a || b || c', { a: '', b: '', c: 'x' })); + }); + + test('chained OR all empty', assert => { + assert.notOk(evaluateExpression('a || b || c', { a: '', b: '', c: '' })); + }); + + test('display_template style: last || first || middle', assert => { + assert.ok(evaluateExpression( + 'last || first || middle', + { last: '', first: 'Taro', middle: '' }, + )); + }); + + test('throws on empty expression', assert => { + assert.throws(() => evaluateExpression('', {}), /empty expression/); + }); + + test('throws on trailing garbage', assert => { + assert.throws(() => evaluateExpression('a b', { a: 'x' }), /unexpected/); + }); + + test('throws on unterminated string', assert => { + assert.throws(() => evaluateExpression("a == 'open", {}), /unterminated/); + }); +}); diff --git a/tests/unit/guid-node/workflow/template-evaluator-test.ts b/tests/unit/guid-node/workflow/template-evaluator-test.ts new file mode 100644 index 000000000..a4c49ffa5 --- /dev/null +++ b/tests/unit/guid-node/workflow/template-evaluator-test.ts @@ -0,0 +1,258 @@ +import { + evaluateTemplate, + hasTemplateDirectives, +} from 'ember-osf-web/guid-node/workflow/-components/wizard-form/template-evaluator'; +import { module, test } from 'qunit'; + +module('Unit | Workflow | template-evaluator', () => { + // -- hasTemplateDirectives ------------------------------------------------ + + test('detects {{ }}', assert => { + assert.ok(hasTemplateDirectives('Hello {{ name }}')); + }); + + test('detects {% %}', assert => { + assert.ok(hasTemplateDirectives('{% if x %}yes{% endif %}')); + }); + + test('plain text has no directives', assert => { + assert.notOk(hasTemplateDirectives('no directives here')); + }); + + // -- variable interpolation ----------------------------------------------- + + test('interpolates variable', assert => { + assert.equal(evaluateTemplate('Hello {{ name }}!', { name: 'Alice' }), 'Hello Alice!'); + }); + + test('missing variable renders empty', assert => { + assert.equal(evaluateTemplate('Hello {{ name }}!', {}), 'Hello !'); + }); + + test('multiple variables', assert => { + assert.equal( + evaluateTemplate('{{ first }} {{ last }}', { first: 'A', last: 'B' }), + 'A B', + ); + }); + + // -- if/else/endif -------------------------------------------------------- + + test('if truthy renders body', assert => { + assert.equal( + evaluateTemplate('{% if show %}visible{% endif %}', { show: 'yes' }), + 'visible', + ); + }); + + test('if falsy skips body', assert => { + assert.equal( + evaluateTemplate('{% if show %}visible{% endif %}', { show: '' }), + '', + ); + }); + + test('if/else renders else branch when falsy', assert => { + assert.equal( + evaluateTemplate('{% if show %}yes{% else %}no{% endif %}', { show: '' }), + 'no', + ); + }); + + test('if/elif/else chain', assert => { + assert.equal( + evaluateTemplate( + '{% if a %}A{% elif b %}B{% else %}C{% endif %}', + { a: '', b: 'x' }, + ), + 'B', + ); + }); + + test('nested if', assert => { + assert.equal( + evaluateTemplate( + '{% if a %}{% if b %}AB{% endif %}{% endif %}', + { a: 'x', b: 'y' }, + ), + 'AB', + ); + }); + + test('nested if with outer false', assert => { + assert.equal( + evaluateTemplate( + '{% if a %}{% if b %}AB{% endif %}{% endif %}', + { a: '', b: 'y' }, + ), + '', + ); + }); + + // -- expressions in if ---------------------------------------------------- + + test('if with or expression', assert => { + assert.equal( + evaluateTemplate('{% if a or b %}yes{% endif %}', { a: '', b: 'x' }), + 'yes', + ); + }); + + test('if with and expression', assert => { + assert.equal( + evaluateTemplate('{% if a and b %}yes{% endif %}', { a: 'x', b: '' }), + '', + ); + }); + + test('if with not expression', assert => { + assert.equal( + evaluateTemplate('{% if not a %}empty{% endif %}', { a: '' }), + 'empty', + ); + }); + + test('if with comparison', assert => { + assert.equal( + evaluateTemplate("{% if status == 'done' %}ok{% endif %}", { status: 'done' }), + 'ok', + ); + }); + + // -- for loop ------------------------------------------------------------- + + test('for loop iterates', assert => { + assert.equal( + evaluateTemplate('{% for x in items %}[{{ x }}]{% endfor %}', { items: ['a', 'b'] }), + '[a][b]', + ); + }); + + test('for loop with empty array', assert => { + assert.equal( + evaluateTemplate('{% for x in items %}[{{ x }}]{% endfor %}', { items: [] }), + '', + ); + }); + + test('for loop with object access', assert => { + assert.equal( + evaluateTemplate( + '{% for p in people %}{{ p.name }} {% endfor %}', + { people: [{ name: 'A' }, { name: 'B' }] }, + ), + 'A B ', + ); + }); + + // -- filters -------------------------------------------------------------- + + test('default filter on missing value', assert => { + assert.equal( + evaluateTemplate("{{ name | default('N/A') }}", {}), + 'N/A', + ); + }); + + test('default filter on present value', assert => { + assert.equal( + evaluateTemplate("{{ name | default('N/A') }}", { name: 'Alice' }), + 'Alice', + ); + }); + + test('length filter', assert => { + assert.equal( + evaluateTemplate('{{ items | length }}', { items: [1, 2, 3] }), + '3', + ); + }); + + // -- whitespace trimming -------------------------------------------------- + + test('trim before with {%-', assert => { + assert.equal( + evaluateTemplate('hello {%- if true %} world{% endif %}', {}), + 'hello world', + ); + }); + + test('trim after with -%}', assert => { + assert.equal( + evaluateTemplate('{% if true -%} hello{% endif %}', {}), + 'hello', + ); + }); + + // -- dot access and bracket access ---------------------------------------- + + test('dot access', assert => { + assert.equal( + evaluateTemplate('{{ user.name }}', { user: { name: 'Bob' } }), + 'Bob', + ); + }); + + test('bracket access', assert => { + assert.equal( + evaluateTemplate("{{ data['key'] }}", { data: { key: 'val' } }), + 'val', + ); + }); + + // -- display_template real-world pattern ----------------------------------- + + test('display_template name pattern with all fields', assert => { + const tpl = '{% if last or first %}{% if last %}{{ last }}, {% endif %}{{ first }} {{ middle }}{% endif %}'; + assert.equal( + evaluateTemplate(tpl, { last: 'Smith', first: 'John', middle: 'A' }), + 'Smith, John A', + ); + }); + + test('display_template name pattern with only first', assert => { + const tpl = '{% if last or first %}{% if last %}{{ last }}, {% endif %}{{ first }} {{ middle }}{% endif %}'; + assert.equal( + evaluateTemplate(tpl, { last: '', first: 'Taro', middle: '' }), + 'Taro ', + ); + }); + + test('display_template name pattern with all empty', assert => { + const tpl = '{% if last or first %}{% if last %}{{ last }}, {% endif %}{{ first }} {{ middle }}{% endif %}'; + assert.equal( + evaluateTemplate(tpl, { last: '', first: '', middle: '' }), + '', + ); + }); + + // -- error handling ------------------------------------------------------- + + test('throws on unclosed if', assert => { + assert.throws( + () => evaluateTemplate('{% if x %}hello', { x: 'y' }), + /unclosed/i, + ); + }); + + test('throws on unclosed {{', assert => { + assert.throws( + () => evaluateTemplate('{{ name', {}), + /unclosed/i, + ); + }); + + test('throws on unknown tag', assert => { + assert.throws( + () => evaluateTemplate('{% while true %}{% endwhile %}', {}), + /unknown tag/i, + ); + }); + + test('throws on unknown filter', assert => { + assert.throws( + () => evaluateTemplate('{{ x | bogus }}', { x: 'v' }), + /unknown filter/i, + ); + }); +}); diff --git a/translations/en-us.yml b/translations/en-us.yml index a70829bd0..a3865b3e4 100644 --- a/translations/en-us.yml +++ b/translations/en-us.yml @@ -645,7 +645,7 @@ node: metadata: new_report_modal: title: Select metadata schema - info: 'The default is 「Metadata registration of publicly funded research data」.