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"); - }); -});