@projectcaluma/ember-form-builder 11.1.5 → 11.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -34,7 +34,7 @@
34
34
  @hint={{t "caluma.form-builder.question.type-disabled"}}
35
35
  @name="__typename"
36
36
  @required={{true}}
37
- @disabled={{@slug}}
37
+ @disabled={{not (is-empty @slug)}}
38
38
  @on-update={{changeset-set f.model "__typename"}}
39
39
  />
40
40
 
@@ -47,39 +47,16 @@
47
47
 
48
48
  <div uk-grid class="uk-grid-small uk-margin">
49
49
  <div class="uk-width-expand">
50
- {{#if (or @slug (not this.prefix))}}
51
- <f.input
52
- @name="slug"
53
- @label={{t "caluma.form-builder.question.slug"}}
54
- @required={{true}}
55
- @disabled={{@slug}}
56
- @on-update={{this.updateSlug}}
57
- />
58
- {{else}}
59
- <f.input
60
- @name="slug"
61
- @label={{t "caluma.form-builder.question.slug"}}
62
- @required={{true}}
63
- @disabled={{@slug}}
64
- @on-update={{this.updateSlug value="target.value"}}
65
- as |fi|
66
- >
67
- <div class="cfb-prefixed">
68
- <span class="cfb-prefixed-slug">{{this.prefix}}</span>
69
-
70
- <f.input
71
- @model={{fi.model}}
72
- @name={{fi.name}}
73
- @value={{fi.value}}
74
- @update={{fi.update}}
75
- @setDirty={{fi.setDirty}}
76
- @inputId={{fi.inputId}}
77
- @isValid={{fi.isValid}}
78
- @isInvalid={{fi.isInvalid}}
79
- />
80
- </div>
81
- </f.input>
82
- {{/if}}
50
+ <f.input
51
+ @name="slug"
52
+ @label={{t "caluma.form-builder.question.slug"}}
53
+ @required={{true}}
54
+ @disabled={{not (is-empty @slug)}}
55
+ @renderComponent={{component
56
+ "cfb-slug-input"
57
+ onUnlink=(fn (mut this.slugUnlinked) true)
58
+ }}
59
+ />
83
60
  </div>
84
61
 
85
62
  {{#if
@@ -459,7 +436,7 @@
459
436
 
460
437
  <div class="uk-text-right">
461
438
  <f.submit
462
- @disabled={{or f.loading f.model.isInvalid}}
439
+ @disabled={{f.loading}}
463
440
  @label={{t "caluma.form-builder.global.save"}}
464
441
  />
465
442
  </div>
@@ -2,13 +2,12 @@ import { A } from "@ember/array";
2
2
  import { action } from "@ember/object";
3
3
  import { inject as service } from "@ember/service";
4
4
  import { camelize } from "@ember/string";
5
- import { macroCondition, isTesting } from "@embroider/macros";
6
5
  import Component from "@glimmer/component";
7
6
  import { tracked } from "@glimmer/tracking";
8
7
  import { queryManager } from "ember-apollo-client";
9
8
  import Changeset from "ember-changeset";
10
9
  import lookupValidator from "ember-changeset-validations";
11
- import { dropTask, restartableTask, task, timeout } from "ember-concurrency";
10
+ import { dropTask, restartableTask, task } from "ember-concurrency";
12
11
 
13
12
  import { hasQuestionType } from "@projectcaluma/ember-core/helpers/has-question-type";
14
13
  import slugify from "@projectcaluma/ember-core/utils/slugify";
@@ -37,9 +36,9 @@ import saveTableQuestionMutation from "@projectcaluma/ember-form-builder/gql/mut
37
36
  import saveTextQuestionMutation from "@projectcaluma/ember-form-builder/gql/mutations/save-text-question.graphql";
38
37
  import saveTextareaQuestionMutation from "@projectcaluma/ember-form-builder/gql/mutations/save-textarea-question.graphql";
39
38
  import allDataSourcesQuery from "@projectcaluma/ember-form-builder/gql/queries/all-data-sources.graphql";
40
- import checkQuestionSlugQuery from "@projectcaluma/ember-form-builder/gql/queries/check-question-slug.graphql";
41
39
  import formEditorQuestionQuery from "@projectcaluma/ember-form-builder/gql/queries/form-editor-question.graphql";
42
40
  import formListQuery from "@projectcaluma/ember-form-builder/gql/queries/form-list.graphql";
41
+ import optionValidations from "@projectcaluma/ember-form-builder/validations/option";
43
42
  import validations from "@projectcaluma/ember-form-builder/validations/question";
44
43
 
45
44
  export const TYPES = {
@@ -77,9 +76,9 @@ export default class CfbFormEditorQuestion extends Component {
77
76
  @service notification;
78
77
  @service intl;
79
78
  @service calumaOptions;
79
+
80
80
  @queryManager apollo;
81
81
 
82
- @tracked linkSlug = true;
83
82
  @tracked changeset;
84
83
 
85
84
  @restartableTask
@@ -209,13 +208,10 @@ export default class CfbFormEditorQuestion extends Component {
209
208
  }
210
209
 
211
210
  getInput(changeset) {
212
- const slug =
213
- ((!this.args.slug && this.prefix) || "") + changeset.get("slug");
214
-
215
211
  const rawMeta = changeset.get("meta");
216
212
 
217
213
  const input = {
218
- slug,
214
+ slug: changeset.get("slug"),
219
215
  label: changeset.get("label"),
220
216
  isHidden: changeset.get("isHidden"),
221
217
  infoText: changeset.get("infoText"),
@@ -289,14 +285,14 @@ export default class CfbFormEditorQuestion extends Component {
289
285
 
290
286
  _getMultipleChoiceQuestionInput(changeset) {
291
287
  return {
292
- options: changeset.get("options.edges").map(({ node: { slug } }) => slug),
288
+ options: changeset.get("options").map(({ slug }) => slug),
293
289
  hintText: changeset.get("hintText"),
294
290
  };
295
291
  }
296
292
 
297
293
  _getChoiceQuestionInput(changeset) {
298
294
  return {
299
- options: changeset.get("options.edges").map(({ node: { slug } }) => slug),
295
+ options: changeset.get("options").map(({ slug }) => slug),
300
296
  hintText: changeset.get("hintText"),
301
297
  };
302
298
  }
@@ -358,14 +354,16 @@ export default class CfbFormEditorQuestion extends Component {
358
354
  @task
359
355
  *saveOptions(changeset) {
360
356
  yield Promise.all(
361
- (changeset.get("options.edges") || []).map(async ({ node: option }) => {
362
- const { label, slug, isArchived } = option;
363
-
364
- await this.apollo.mutate({
365
- mutation: saveOptionMutation,
366
- variables: { input: { label, slug, isArchived } },
367
- });
368
- })
357
+ (changeset.get("options") || [])
358
+ .filter((option) => option.get("isDirty"))
359
+ .map(async (option) => {
360
+ const { label, slug, isArchived } = option;
361
+
362
+ await this.apollo.mutate({
363
+ mutation: saveOptionMutation,
364
+ variables: { input: { label, slug, isArchived } },
365
+ });
366
+ })
369
367
  );
370
368
  }
371
369
 
@@ -447,39 +445,36 @@ export default class CfbFormEditorQuestion extends Component {
447
445
  }
448
446
  }
449
447
 
450
- @restartableTask
451
- *validateSlug(slug, changeset) {
452
- /* istanbul ignore next */
453
- if (macroCondition(isTesting())) {
454
- // no timeout
455
- } else {
456
- yield timeout(500);
457
- }
458
-
459
- const res = yield this.apollo.query(
460
- {
461
- query: checkQuestionSlugQuery,
462
- variables: { slug },
463
- },
464
- "allQuestions.edges"
465
- );
466
-
467
- if (res && res.length) {
468
- changeset.pushErrors(
469
- "slug",
470
- this.intl.t("caluma.form-builder.validations.question.slug")
471
- );
472
- }
473
- }
474
-
475
448
  @action
476
449
  async fetchData() {
477
450
  await this.data.perform();
478
451
  await this.availableForms.perform();
479
452
  await this.availableDataSources.perform();
480
453
  if (this.model) {
454
+ const options = this.model.options?.edges?.map(
455
+ (edge) =>
456
+ new Changeset(
457
+ { ...edge.node, slugUnlinked: false, question: this.model.slug },
458
+ lookupValidator(optionValidations),
459
+ optionValidations
460
+ )
461
+ ) ?? [
462
+ new Changeset(
463
+ {
464
+ id: undefined,
465
+ label: "",
466
+ slug: "",
467
+ isArchived: false,
468
+ slugUnlinked: false,
469
+ question: this.model.slug,
470
+ },
471
+ lookupValidator(optionValidations),
472
+ optionValidations
473
+ ),
474
+ ];
475
+
481
476
  this.changeset = new Changeset(
482
- this.model,
477
+ { ...this.model, options },
483
478
  lookupValidator(validations),
484
479
  validations
485
480
  );
@@ -490,23 +485,16 @@ export default class CfbFormEditorQuestion extends Component {
490
485
  updateLabel(value, changeset) {
491
486
  changeset.set("label", value);
492
487
 
493
- if (!this.args.slug && this.linkSlug) {
494
- const slug = slugify(value, { locale: this.intl.primaryLocale });
488
+ if (!this.args.slug && !this.slugUnlinked) {
489
+ const slugifiedLabel = slugify(value, {
490
+ locale: this.intl.primaryLocale,
491
+ });
492
+ const slug = slugifiedLabel ? this.prefix + slugifiedLabel : "";
495
493
 
496
494
  changeset.set("slug", slug);
497
-
498
- this.validateSlug.perform(this.prefix + slug, changeset);
499
495
  }
500
496
  }
501
497
 
502
- @action
503
- updateSlug(value, changeset) {
504
- changeset.set("slug", value);
505
- this.linkSlug = false;
506
-
507
- this.validateSlug.perform(this.prefix + value, changeset);
508
- }
509
-
510
498
  @action
511
499
  updateSubForm(value, changeset) {
512
500
  changeset.set("subForm.slug", value.slug);
@@ -0,0 +1,33 @@
1
+ <div class="uk-margin">
2
+ <@labelComponent />
3
+
4
+ <div class="uk-form-controls">
5
+ <div class="cfb-slug-input">
6
+ {{#if (and (not (or @disabled @hidePrefix)) this.prefix)}}
7
+ <span
8
+ class="cfb-slug-input__prefix
9
+ {{if @isValid 'uk-text-success'}}
10
+ {{if @isInvalid 'uk-text-danger'}}"
11
+ {{did-insert this.calculatePadding}}
12
+ >
13
+ {{this.prefix}}
14
+ </span>
15
+ {{/if}}
16
+ <input
17
+ id={{@inputId}}
18
+ name={{or @inputName @name}}
19
+ type="text"
20
+ class="uk-input cfb-slug-input__input
21
+ {{if @isValid 'uk-form-success'}}
22
+ {{if @isInvalid 'uk-form-danger'}}"
23
+ value={{this.displayValue}}
24
+ disabled={{@disabled}}
25
+ style={{this.inputStyle}}
26
+ {{on "input" this.update}}
27
+ />
28
+ </div>
29
+ </div>
30
+
31
+ <@hintComponent />
32
+ <@errorComponent />
33
+ </div>
@@ -0,0 +1,45 @@
1
+ import { action } from "@ember/object";
2
+ import { inject as service } from "@ember/service";
3
+ import { htmlSafe } from "@ember/template";
4
+ import Component from "@glimmer/component";
5
+ import { tracked } from "@glimmer/tracking";
6
+
7
+ export default class CfbSlugInputComponent extends Component {
8
+ @service calumaOptions;
9
+ @service intl;
10
+
11
+ @tracked padding = null;
12
+
13
+ get prefix() {
14
+ const prefix = this.args.prefix ?? this.calumaOptions.namespace ?? null;
15
+
16
+ return prefix ? `${prefix}-` : "";
17
+ }
18
+
19
+ get inputStyle() {
20
+ return this.padding ? htmlSafe(`padding-left: ${this.padding};`) : "";
21
+ }
22
+
23
+ get displayValue() {
24
+ if (this.args.disabled && !this.args.hidePrefix) {
25
+ return this.args.value;
26
+ }
27
+
28
+ return this.args.value?.replace(new RegExp(`^${this.prefix}`), "") ?? "";
29
+ }
30
+
31
+ @action
32
+ calculatePadding(element) {
33
+ const prefixWidth = element.clientWidth;
34
+ const prefixMargin = window.getComputedStyle(element).marginLeft;
35
+
36
+ this.padding = `calc(${prefixWidth}px + ${prefixMargin})`;
37
+ }
38
+
39
+ @action
40
+ update({ target: { value } }) {
41
+ this.args.update(value ? this.prefix + value : "");
42
+ this.args.setDirty();
43
+ this.args.onUnlink?.();
44
+ }
45
+ }
@@ -1,10 +1,5 @@
1
1
  query CheckFormSlug($slug: String!) {
2
2
  allForms(filter: [{ slugs: [$slug] }]) {
3
- edges {
4
- node {
5
- id
6
- slug
7
- }
8
- }
3
+ totalCount
9
4
  }
10
5
  }
@@ -0,0 +1,19 @@
1
+ query CheckOptionSlug($slug: String!, $question: String!) {
2
+ allQuestions(filter: [{ slugs: [$question] }]) {
3
+ edges {
4
+ node {
5
+ id
6
+ ... on ChoiceQuestion {
7
+ options(filter: [{ slug: $slug }]) {
8
+ totalCount
9
+ }
10
+ }
11
+ ... on MultipleChoiceQuestion {
12
+ options(filter: [{ slug: $slug }]) {
13
+ totalCount
14
+ }
15
+ }
16
+ }
17
+ }
18
+ }
19
+ }
@@ -1,10 +1,5 @@
1
1
  query CheckQuestionSlug($slug: String!) {
2
2
  allQuestions(filter: [{ slugs: [$slug] }]) {
3
- edges {
4
- node {
5
- id
6
- slug
7
- }
8
- }
3
+ totalCount
9
4
  }
10
5
  }
@@ -0,0 +1,9 @@
1
+ import application from "@projectcaluma/ember-form-builder/-private/application";
2
+
3
+ export function initialize(appInstance) {
4
+ application.instance = appInstance;
5
+ }
6
+
7
+ export default {
8
+ initialize,
9
+ };
@@ -1,14 +1,11 @@
1
1
  import {
2
2
  validatePresence,
3
3
  validateLength,
4
- validateFormat,
5
4
  } from "ember-changeset-validations/validators";
6
5
 
6
+ import slugValidation from "@projectcaluma/ember-form-builder/validators/slug";
7
+
7
8
  export default {
8
9
  name: [validatePresence(true), validateLength({ max: 255 })],
9
- slug: [
10
- validatePresence(true),
11
- validateLength({ max: 50 }),
12
- validateFormat({ regex: /^[a-z0-9-]+$/ }),
13
- ],
10
+ slug: slugValidation({ type: "form", maxLength: 50 }),
14
11
  };
@@ -3,10 +3,9 @@ import {
3
3
  validateLength,
4
4
  } from "ember-changeset-validations/validators";
5
5
 
6
- import and from "@projectcaluma/ember-form-builder/utils/and";
7
- import validateSlug from "@projectcaluma/ember-form-builder/validators/slug";
6
+ import slugValidation from "@projectcaluma/ember-form-builder/validators/slug";
8
7
 
9
8
  export default {
10
- label: and(validatePresence(true), validateLength({ max: 1024 })),
11
- slug: validateSlug(),
9
+ label: [validatePresence(true), validateLength({ max: 1024 })],
10
+ slug: slugValidation({ type: "option" }),
12
11
  };
@@ -11,12 +11,12 @@ import and from "@projectcaluma/ember-form-builder/utils/and";
11
11
  import or from "@projectcaluma/ember-form-builder/utils/or";
12
12
  import validateJexl from "@projectcaluma/ember-form-builder/validators/jexl";
13
13
  import validateMeta from "@projectcaluma/ember-form-builder/validators/meta";
14
- import validateSlug from "@projectcaluma/ember-form-builder/validators/slug";
14
+ import slugValidation from "@projectcaluma/ember-form-builder/validators/slug";
15
15
  import validateType from "@projectcaluma/ember-form-builder/validators/type";
16
16
 
17
17
  export default {
18
18
  label: and(validatePresence(true), validateLength({ max: 1024 })),
19
- slug: validateSlug(),
19
+ slug: slugValidation({ type: "question" }),
20
20
 
21
21
  hintText: or(
22
22
  validateType("FormQuestion", true),
@@ -1,25 +1,7 @@
1
- import Changeset from "ember-changeset";
2
- import lookupValidator from "ember-changeset-validations";
3
- import { Promise, all } from "rsvp";
4
-
5
- import OptionValidations from "../validations/option";
6
-
7
1
  export default function validateOptions() {
8
- return (_, value) => {
9
- return new Promise((resolve) => {
10
- all(
11
- value.edges.map(async ({ node: option }) => {
12
- const cs = new Changeset(
13
- option,
14
- lookupValidator(OptionValidations),
15
- OptionValidations
16
- );
17
-
18
- await cs.validate();
19
-
20
- return cs.get("isValid");
21
- })
22
- ).then((res) => resolve(res.every(Boolean) || "Invalid options"));
23
- });
2
+ return (_, newValue) => {
3
+ return (
4
+ newValue.every((option) => option.get("isValid")) || "Invalid options"
5
+ );
24
6
  };
25
7
  }
@@ -1,16 +1,118 @@
1
+ import { setOwner } from "@ember/application";
2
+ import { inject as service } from "@ember/service";
3
+ import { macroCondition, isTesting, importSync } from "@embroider/macros";
4
+ import { queryManager } from "ember-apollo-client";
1
5
  import {
2
6
  validatePresence,
3
7
  validateLength,
4
8
  validateFormat,
5
9
  } from "ember-changeset-validations/validators";
10
+ import { timeout, restartableTask, didCancel } from "ember-concurrency";
6
11
 
7
- import and from "@projectcaluma/ember-form-builder/utils/and";
12
+ import checkFormSlugQuery from "@projectcaluma/ember-form-builder/gql/queries/check-form-slug.graphql";
13
+ import checkOptionSlugQuery from "@projectcaluma/ember-form-builder/gql/queries/check-option-slug.graphql";
14
+ import checkQuestionSlugQuery from "@projectcaluma/ember-form-builder/gql/queries/check-question-slug.graphql";
8
15
 
9
- const validateSlug = () =>
10
- and(
11
- validatePresence(true),
12
- validateLength({ max: 127 }),
13
- validateFormat({ regex: /^[a-z0-9-]+$/ })
14
- );
16
+ export class SlugUniquenessValidator {
17
+ @service intl;
18
+
19
+ @queryManager apollo;
20
+
21
+ queries = {
22
+ form: checkFormSlugQuery,
23
+ question: checkQuestionSlugQuery,
24
+ option: checkOptionSlugQuery,
25
+ };
26
+
27
+ cache = {
28
+ form: {},
29
+ question: {},
30
+ option: {},
31
+ };
32
+
33
+ constructor(type) {
34
+ this.type = type;
35
+ }
36
+
37
+ async validate(key, newValue, oldValue, changes, context) {
38
+ const application = importSync(
39
+ "@projectcaluma/ember-form-builder/-private/application"
40
+ ).default;
41
+
42
+ setOwner(this, application.instance);
43
+
44
+ // If the object already exists or the slug is empty we don't need to check
45
+ // for uniqueness
46
+ if (context.id || !newValue) {
47
+ return true;
48
+ }
49
+
50
+ let isUnique = this.cache[this.type][newValue];
51
+
52
+ if (isUnique === undefined) {
53
+ try {
54
+ // Uniqueness of the slug was not cached, we need to check with the API
55
+ isUnique = await this._validate.perform(newValue, context);
56
+ } catch (error) {
57
+ // Validation task was canceled because we debounce it so we don't cause
58
+ // too many requests on input. This cancelation means that there is
59
+ // another validation ongoing which will then return the needed value.
60
+ // For now, we can just mark it as valid.
61
+ isUnique = didCancel(error);
62
+ }
63
+ }
15
64
 
16
- export default validateSlug;
65
+ return (
66
+ isUnique ||
67
+ this.intl.t(`caluma.form-builder.validations.${this.type}.slug`)
68
+ );
69
+ }
70
+
71
+ @restartableTask
72
+ *_validate(slug, context) {
73
+ /* istanbul ignore next */
74
+ if (macroCondition(isTesting())) {
75
+ // no timeout
76
+ } else {
77
+ yield timeout(500);
78
+ }
79
+
80
+ let count = Infinity;
81
+
82
+ try {
83
+ const response = yield this.apollo.query({
84
+ query: this.queries[this.type],
85
+ variables:
86
+ this.type === "option"
87
+ ? { slug, question: context.question }
88
+ : { slug },
89
+ });
90
+
91
+ if (this.type === "form") {
92
+ count = response.allForms.totalCount;
93
+ } else if (this.type === "question") {
94
+ count = response.allQuestions.totalCount;
95
+ } else if (this.type === "option") {
96
+ count = response.allQuestions.edges[0].node.options.totalCount;
97
+ }
98
+ } catch (error) {
99
+ // do nothing, which will result in count being Infinity which will return
100
+ // a validation error
101
+ }
102
+
103
+ const isUnique = count === 0;
104
+
105
+ this.cache[this.type][slug] = isUnique;
106
+
107
+ return isUnique;
108
+ }
109
+ }
110
+
111
+ export default function slugValidation({ type, maxLength = 127 }) {
112
+ return [
113
+ validatePresence(true),
114
+ validateLength({ max: maxLength }),
115
+ validateFormat({ regex: /^[a-z0-9-]+$/ }),
116
+ new SlugUniquenessValidator(type),
117
+ ];
118
+ }
@@ -0,0 +1 @@
1
+ export { default } from "@projectcaluma/ember-form-builder/components/cfb-slug-input";
@@ -0,0 +1,4 @@
1
+ export {
2
+ default,
3
+ initialize,
4
+ } from "@projectcaluma/ember-form-builder/instance-initializers/application";
@@ -3,22 +3,13 @@
3
3
  @import "../cfb-form-editor/question-list/item";
4
4
  @import "../cfb-form-editor/question";
5
5
  @import "../cfb-navigation";
6
+ @import "../cfb-slug-input";
6
7
  @import "../cfb-uikit-powerselect";
7
8
 
8
9
  .cfb-pointer {
9
10
  cursor: pointer;
10
11
  }
11
12
 
12
- .cfb-prefixed {
13
- display: flex;
14
-
15
- &-slug {
16
- line-height: 40px;
17
- padding-right: 10px;
18
- white-space: nowrap;
19
- }
20
- }
21
-
22
13
  .cfb-code-editor {
23
14
  font-family: $base-code-font-family;
24
15
  letter-spacing: normal;
@@ -0,0 +1,12 @@
1
+ .cfb-slug-input {
2
+ position: relative;
3
+
4
+ &__prefix {
5
+ position: absolute;
6
+ margin-left: $form-padding-horizontal;
7
+ left: 0;
8
+ top: 50%;
9
+ transform: translateY(-50%);
10
+ color: $global-muted-color;
11
+ }
12
+ }
@@ -4,14 +4,24 @@ module.exports = {
4
4
  normalizeEntityName() {},
5
5
 
6
6
  afterInstall() {
7
+ /**
8
+ * Automatically install all ember addons that expose helpers / components
9
+ * used in templates of the addon itself. Other dependencies that are only
10
+ * used in JS code don't need to be installed in the host app and therefore
11
+ * don't have to be included here.
12
+ */
7
13
  return this.addAddonsToProject({
8
14
  packages: [
15
+ { name: "@ember/legacy-built-in-components" },
9
16
  { name: "@projectcaluma/ember-core" },
10
17
  { name: "@projectcaluma/ember-form" },
18
+ { name: "ember-changeset" },
19
+ { name: "ember-composable-helpers" },
11
20
  { name: "ember-engines" },
12
- { name: "ember-math-helpers" },
13
21
  { name: "ember-flatpickr" },
22
+ { name: "ember-math-helpers" },
14
23
  { name: "ember-power-select" },
24
+ { name: "ember-validated-form" },
15
25
  ],
16
26
  });
17
27
  },