From ed31b2b7b905aeb94ffdf0fc44954a5a830c8081 Mon Sep 17 00:00:00 2001 From: Falk Date: Tue, 8 Mar 2022 16:24:25 +0100 Subject: [PATCH] feat(emeisOptions): passing options as a function - this enable to pass options for meta-fields of type choice as static list or as a function --- README.md | 14 +- addon/components/meta-field.hbs | 32 +++ addon/components/meta-field.js | 60 +++++ addon/components/meta-fields.hbs | 30 --- addon/components/meta-fields.js | 14 -- addon/helpers/eval-meta.js | 8 - addon/templates/scopes/edit.hbs | 8 +- addon/templates/users/edit.hbs | 7 +- app/components/meta-field.js | 1 + app/components/meta-fields.js | 1 - app/helpers/eval-meta.js | 1 - tests/dummy/app/services/emeis-options.js | 49 ++-- .../integration/components/meta-field-test.js | 223 ++++++++++++++++++ .../components/meta-fields-test.js | 199 ---------------- tests/integration/helpers/eval-meta-test.js | 41 ---- 15 files changed, 370 insertions(+), 318 deletions(-) create mode 100644 addon/components/meta-field.hbs create mode 100644 addon/components/meta-field.js delete mode 100644 addon/components/meta-fields.hbs delete mode 100644 addon/components/meta-fields.js delete mode 100644 addon/helpers/eval-meta.js create mode 100644 app/components/meta-field.js delete mode 100644 app/components/meta-fields.js delete mode 100644 app/helpers/eval-meta.js create mode 100644 tests/integration/components/meta-field-test.js delete mode 100644 tests/integration/components/meta-fields-test.js delete mode 100644 tests/integration/helpers/eval-meta-test.js diff --git a/README.md b/README.md index ec5de6dd..b1bc27ee 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,12 @@ export default class EmeisOptionsService extends Service { { slug: "test-input-2", label: "some.translation.key", + options: [ // insert a static list of options (value, label), or a (async) function which resolves to a list of options + { + value: "option-1", + label: "Option one" + } + ], type: "choice", visible: () => true, readOnly: false @@ -134,15 +140,19 @@ export default class EmeisOptionsService extends Service { _Watch out_ - the translation key has to be present in your local translation files. -There are special options available for `type` and `visible` properties. +There are special options available for `options`, `type` and `visible` properties. #### **type** - meta field Defines the type of the output component and can either be a _text_ or a _choice_. +#### **options** - meta field + +In combination with `type:"choice"` the options can be a list of options (`{value, label}`) or a (async) function which resolves to a list of options. + #### **visible** & **readOnly** meta field -Accepts a boolean value for static visibility or a function which evaluates to a boolean value. Submitted functions will evaluate live while rendering. +Accepts a boolean value for static visibility or a (async) function which evaluates to a boolean value. Submitted functions will evaluate live while rendering. The evaluation function will receive the current model as argument. For instance if you are on the scope route, you will receive the [scope model](addon/models/scope.js) as first argument. Same for [user](addon/models/user.js) | [role](addon/models/role.js) | [permission](addon/models/permission.js) diff --git a/addon/components/meta-field.hbs b/addon/components/meta-field.hbs new file mode 100644 index 00000000..dbaced46 --- /dev/null +++ b/addon/components/meta-field.hbs @@ -0,0 +1,32 @@ +{{#if this.visible.value}} + + {{#if (eq @field.type "choice")}} + {{#if this.options.isRunning}} + + {{else}} + + {{optional-translate option.label}} + + {{/if}} + {{/if}} + {{#if (eq @field.type "text")}} + + {{/if}} + +{{/if}} \ No newline at end of file diff --git a/addon/components/meta-field.js b/addon/components/meta-field.js new file mode 100644 index 00000000..dafd4bb9 --- /dev/null +++ b/addon/components/meta-field.js @@ -0,0 +1,60 @@ +import { assert } from "@ember/debug"; +import { action } from "@ember/object"; +import { inject as service } from "@ember/service"; +import Component from "@glimmer/component"; +import { task } from "ember-concurrency"; +import { useTask } from "ember-resources"; + +export default class MetaFieldComponent extends Component { + @service intl; + + constructor(...args) { + super(...args); + + assert("Must pass a valid field argument", this.args.field); + assert("Must pass a valid model argument", this.args.model); + } + + async evaluateToBoolean(expression) { + if (typeof expression === "boolean") { + return expression; + } + if (typeof expression === "function") { + return await expression(this.args.model); + } + if (typeof expression === "string") { + return expression === "true"; + } + return false; + } + + visible = useTask(this, this.evalVisible, () => [this.args.field.visible]); + readOnly = useTask(this, this.evalReadOnly, () => [this.args.field.readOnly]); + options = useTask(this, this.evalOptions, () => [this.args.field.options]); + + @task + *evalVisible(visible) { + return yield this.evaluateToBoolean(visible); + } + + @task + *evalReadOnly(readOnly) { + return yield this.evaluateToBoolean(readOnly); + } + + @task + *evalOptions(options) { + // options may be a (async) function or a complex property + if (typeof options !== "function") { + return options; + } + return yield options(this.args.model); + } + + @action + updateMetaField(field, model, optionOrEvent) { + const value = optionOrEvent?.target?.value ?? optionOrEvent?.value; + model.metainfo = { ...model.metainfo, [field.slug]: value }; + model.notifyPropertyChange("metainfo"); + } +} diff --git a/addon/components/meta-fields.hbs b/addon/components/meta-fields.hbs deleted file mode 100644 index fb4421c8..00000000 --- a/addon/components/meta-fields.hbs +++ /dev/null @@ -1,30 +0,0 @@ -{{#each @fields as |field|}} - {{#if (eval-meta field.visible @model)}} - - {{#if (eq field.type "choice")}} - - {{optional-translate option.label}} - - {{/if}} - {{#if (eq field.type "text")}} - - {{/if}} - - {{/if}} -{{/each}} diff --git a/addon/components/meta-fields.js b/addon/components/meta-fields.js deleted file mode 100644 index a7bba2ea..00000000 --- a/addon/components/meta-fields.js +++ /dev/null @@ -1,14 +0,0 @@ -import { action } from "@ember/object"; -import { inject as service } from "@ember/service"; -import Component from "@glimmer/component"; - -export default class EditFormComponent extends Component { - @service intl; - - @action - updateMetaField(field, model, optionOrEvent) { - const value = optionOrEvent?.target?.value ?? optionOrEvent?.value; - model.metainfo = { ...model.metainfo, [field.slug]: value }; - model.notifyPropertyChange("metainfo"); - } -} diff --git a/addon/helpers/eval-meta.js b/addon/helpers/eval-meta.js deleted file mode 100644 index 4f746d0a..00000000 --- a/addon/helpers/eval-meta.js +++ /dev/null @@ -1,8 +0,0 @@ -import { helper } from "@ember/component/helper"; - -export default helper(function evalMeta([expression, model]) { - if (typeof expression === "boolean") return expression; - if (typeof expression === "function") return expression(model); - if (typeof expression === "string") return expression === "true"; - return false; -}); diff --git a/addon/templates/scopes/edit.hbs b/addon/templates/scopes/edit.hbs index c6c7686f..8c9d8c36 100644 --- a/addon/templates/scopes/edit.hbs +++ b/addon/templates/scopes/edit.hbs @@ -38,10 +38,10 @@ - + {{#each this.metaFields as |field|}} + + {{/each}} +
diff --git a/addon/templates/users/edit.hbs b/addon/templates/users/edit.hbs index f27151ba..210cde0b 100644 --- a/addon/templates/users/edit.hbs +++ b/addon/templates/users/edit.hbs @@ -131,10 +131,9 @@ {{/if}} - + {{#each this.metaFields as |field|}} + + {{/each}} { + await timeout(2000); + return [ + { + value: "Option 1", + label: "emeis.options.meta.scope.options.label-1", // again a ember-intl translation key + }, + { + value: "Option 2", + label: "emeis.options.meta.scope.options.label-2", + }, + { + value: "Option 3", + label: "emeis.options.meta.scope.options.label-3", + }, + ]; + }, visible: () => true, // boolean or function which evaluates to a boolean value readOnly: false, }, diff --git a/tests/integration/components/meta-field-test.js b/tests/integration/components/meta-field-test.js new file mode 100644 index 00000000..dd83d561 --- /dev/null +++ b/tests/integration/components/meta-field-test.js @@ -0,0 +1,223 @@ +import { fillIn, render } from "@ember/test-helpers"; +import { hbs } from "ember-cli-htmlbars"; +import { setupIntl } from "ember-intl/test-support"; +import { selectChoose } from "ember-power-select/test-support"; +import { setupRenderingTest } from "ember-qunit"; +import { module, test } from "qunit"; + +const translations = { + metaExample: "Example for custom choice field", + option1: "Ham", + option2: "Cheese", + metaExample2: "Example for custom text field", + dynamicVisibility: "field with dynamic visibility (visible)", + dynamicVisibility2: "field with dynamic visibility (unvisible)", + dynamicReadOnly: "field with dynamic readOnly state", +}; + +module("Integration | Component | meta-field", function (hooks) { + setupRenderingTest(hooks); + setupIntl(hooks, ["en"], translations); + + hooks.beforeEach(function () { + this.model = { + metainfo: {}, + notifyPropertyChange: () => {}, + }; + }); + + test("it renders meta field of type select", async function (assert) { + assert.expect(3); + + this.set("field", { + slug: "meta-example", + label: "metaExample", + type: "choice", + options: [ + { + value: "option-1", + label: "option1", + }, + { + value: "Option 2", + label: "option2", + }, + ], + visible: true, + readOnly: false, + }); + + await render(hbs` + + `); + + assert.dom(".ember-power-select-trigger").exists(); + assert.dom(this.element).containsText(translations.metaExample); + + await selectChoose(".ember-power-select-trigger", "Ham"); + assert.deepEqual(this.model.metainfo, { + "meta-example": "option-1", + }); + }); + + test("it renders meta field of type text", async function (assert) { + assert.expect(3); + + this.set("field", { + slug: "meta-example-2", + label: "metaExample2", + type: "text", + visible: true, + readOnly: false, + }); + + await render(hbs` + + `); + + assert.dom("[data-test-meta-field-text]").exists({ count: 1 }); + assert.dom(this.element).containsText(translations.metaExample2); + + await fillIn("[data-test-meta-field-text]", "My value"); + assert.deepEqual(this.model.metainfo, { + "meta-example-2": "My value", + }); + }); + + test("it does not render invisible meta fields", async function (assert) { + assert.expect(2); + + this.set("field", { + slug: "invisible", + label: "invisible", + type: "text", + visible: false, + readOnly: true, + }); + + await render(hbs` + + `); + + assert.dom(".ember-power-select-trigger").doesNotExist(); + assert.dom("[data-test-meta-field-text]").doesNotExist(); + }); + + test("it renders fields with dynamically evaluated visibility", async function (assert) { + assert.expect(2); + this.set("field1", { + slug: "dynamic-visibility", + label: "dynamicVisibility", + type: "text", + visible: () => true, + readOnly: true, + }); + + this.set("field2", { + slug: "dynamic-visibility-2", + label: "dynamicVisibility2", + type: "text", + visible: () => 1 > 2, + readOnly: false, + }); + + await render(hbs` + + + `); + + assert.dom("[data-test-meta-field-text='dynamic-visibility']").exists(); + assert + .dom("[data-test-meta-field-text='dynamic-visibility-2']") + .doesNotExist(); + }); + + test("it renders statically disabled meta field", async function (assert) { + assert.expect(1); + this.set("field", { + slug: "static-read-only-text", + label: "staticReadyOnlyText", + type: "text", + visible: true, + readOnly: true, + }); + + await render(hbs` + + `); + + assert + .dom("[data-test-meta-field-text='static-read-only-text']") + .hasAttribute("disabled"); + }); + + test("it renders dynamically disabled meta fields", async function (assert) { + assert.expect(3); + this.set("field1", { + slug: "dynamic-read-only-text", + label: "dynamicReadOnly", + type: "text", + visible: true, + readOnly: (model) => model.name === "readOnly", + }); + + this.set("field2", { + slug: "dynamic-read-only-choice", + label: "dynamicReadOnly", + type: "choice", + options: [ + { + value: "option-1", + label: "option1", + }, + { + value: "Option 2", + label: "option2", + }, + ], + visible: true, + readOnly: (model) => model.name === "readOnly", + }); + + this.model.name = "readOnly"; + + await render(hbs` + + + `); + + assert.dom(".ember-power-select-trigger").hasAttribute("aria-disabled"); + + assert + .dom("[data-test-meta-field-text='dynamic-read-only-text']") + .exists({ count: 1 }); + + assert + .dom("[data-test-meta-field-text='dynamic-read-only-text']") + .hasAttribute("disabled"); + }); +}); diff --git a/tests/integration/components/meta-fields-test.js b/tests/integration/components/meta-fields-test.js deleted file mode 100644 index 83d24f43..00000000 --- a/tests/integration/components/meta-fields-test.js +++ /dev/null @@ -1,199 +0,0 @@ -import Service from "@ember/service"; -import { fillIn, render } from "@ember/test-helpers"; -import { hbs } from "ember-cli-htmlbars"; -import { setupIntl } from "ember-intl/test-support"; -import { selectChoose } from "ember-power-select/test-support"; -import { setupRenderingTest } from "ember-qunit"; -import { module, test } from "qunit"; - -const translations = { - scope: { - metaExample: "Example for custom choice field", - option1: "Ham", - option2: "Cheese", - metaExample2: "Example for custom text field", - dynamicVisibility: "field with dynamic visibility (visible)", - dynamicVisibility2: "field with dynamic visibility (unvisible)", - dynamicReadOnly: "field with dynamic readOnly state", - }, -}; -class EmeisOptionsStub extends Service { - metaFields = { - scope: [ - { - slug: "meta-example", - label: "scope.metaExample", - type: "choice", - options: [ - { - value: "option-1", - label: "scope.option1", - }, - { - value: "Option 2", - label: "scope.option2", - }, - ], - visible: true, - readOnly: false, - }, - { - slug: "meta-example-2", - label: "scope.metaExample2", - type: "text", - visible: true, - readOnly: false, - }, - { - slug: "dynamic-visibility", - label: "scope.dynamicVisibility", - type: "text", - visible: () => true, - readOnly: true, - }, - { - slug: "dynamic-visibility-2", - label: "scope.dynamicVisibility2", - type: "text", - visible: () => 1 > 2, - readOnly: false, - }, - { - slug: "dynamic-readOnly", - label: "scope.dynamicReadOnly", - type: "text", - visible: (model) => model.name === "readOnly", - readOnly: (model) => model.name === "readOnly", - }, - ], - }; -} - -module("Integration | Component | meta-fields", function (hooks) { - setupRenderingTest(hooks); - setupIntl(hooks, ["en"], translations); - - hooks.beforeEach(function () { - this.owner.register("service:emeisOptions", EmeisOptionsStub); - this.emeisOptions = this.owner.lookup("service:emeisOptions"); - - this.model = { - metainfo: {}, - notifyPropertyChange: () => {}, - }; - }); - - test("it renders", async function (assert) { - await render(hbs``); - - assert.dom(this.element).hasText(""); - }); - - test("it renders meta field of type select and text", async function (assert) { - assert.expect(6); - - await render(hbs` - - `); - - assert.dom(".ember-power-select-trigger").exists(); - assert.dom("[data-test-meta-field-text]").exists({ count: 2 }); - - assert.dom(this.element).containsText(translations.scope.metaExample); - assert.dom(this.element).containsText(translations.scope.metaExample2); - - await selectChoose(".ember-power-select-trigger", "Ham"); - assert.deepEqual(this.model.metainfo, { - "meta-example": "option-1", - }); - - await fillIn("[data-test-meta-field-text]", "My value"); - assert.deepEqual(this.model.metainfo, { - "meta-example": "option-1", - "meta-example-2": "My value", - }); - }); - - test("it does not render invisible meta fields", async function (assert) { - assert.expect(2); - - // Set visibility to `false` for each field - this.emeisOptions.metaFields.scope.forEach( - (field) => (field.visible = false) - ); - - await render(hbs` - - `); - - assert.dom(".ember-power-select-trigger").doesNotExist(); - assert.dom("[data-test-meta-field-text]").doesNotExist(); - }); - - test("it renders fields with dynamically evaluated visibility", async function (assert) { - assert.expect(2); - - await render(hbs` - - `); - - assert.dom("[data-test-meta-field-text='dynamic-visibility']").exists(); - assert - .dom("[data-test-meta-field-text='dynamic-visibility-2']") - .doesNotExist(); - }); - - test("it renders disabled meta fields", async function (assert) { - assert.expect(4); - - // Set fields to read-only - this.emeisOptions.metaFields.scope.forEach( - (field) => (field.readOnly = true) - ); - - await render(hbs` - - `); - - assert.dom(".ember-power-select-trigger").exists(); - assert.dom("[data-test-meta-field-text]").exists({ count: 2 }); - - assert.dom(".ember-power-select-trigger").hasAttribute("aria-disabled"); - assert - .dom("[data-test-meta-field-text='dynamic-visibility']") - .hasAttribute("disabled"); - }); - - test("it renders dynamically disabled meta fields", async function (assert) { - assert.expect(2); - - this.model.name = "readOnly"; - - await render(hbs` - - `); - - assert - .dom("[data-test-meta-field-text='dynamic-readOnly']") - .exists({ count: 1 }); - - assert - .dom("[data-test-meta-field-text='dynamic-readOnly']") - .hasAttribute("disabled"); - }); -}); diff --git a/tests/integration/helpers/eval-meta-test.js b/tests/integration/helpers/eval-meta-test.js deleted file mode 100644 index 7dbfbaf3..00000000 --- a/tests/integration/helpers/eval-meta-test.js +++ /dev/null @@ -1,41 +0,0 @@ -import { render } from "@ember/test-helpers"; -import { hbs } from "ember-cli-htmlbars"; -import { setupRenderingTest } from "ember-qunit"; -import { module, test } from "qunit"; - -module("Integration | Helper | eval-meta", function (hooks) { - setupRenderingTest(hooks); - - test("it evaluates strings", async function (assert) { - this.set("inputValue", "1234"); - - await render(hbs`{{eval-meta this.inputValue}}`); - - assert.strictEqual(this.element.textContent, "false"); - - this.set("inputValue", "true"); - assert.strictEqual(this.element.textContent, "true"); - }); - - test("it evaluates booleans", async function (assert) { - this.set("inputValue", true); - - await render(hbs`{{eval-meta this.inputValue}}`); - - assert.strictEqual(this.element.textContent, "true"); - - this.set("inputValue", false); - assert.strictEqual(this.element.textContent, "false"); - }); - - test("it evaluates functions", async function (assert) { - this.set("inputValue", () => true); - - await render(hbs`{{eval-meta this.inputValue}}`); - - assert.strictEqual(this.element.textContent, "true"); - - this.set("inputValue", () => false); - assert.strictEqual(this.element.textContent, "false"); - }); -});