@projectcaluma/ember-form 14.8.3 → 14.9.0

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.
Files changed (54) hide show
  1. package/addon/components/cf-content.hbs +44 -39
  2. package/addon/components/cf-content.js +50 -14
  3. package/addon/components/cf-field/input/checkbox.hbs +3 -1
  4. package/addon/components/cf-field/input/checkbox.js +15 -0
  5. package/addon/components/cf-field/input/date.hbs +38 -31
  6. package/addon/components/cf-field/input/float.hbs +22 -15
  7. package/addon/components/cf-field/input/integer.hbs +22 -15
  8. package/addon/components/cf-field/input/number-separator.hbs +19 -12
  9. package/addon/components/cf-field/input/number-separator.js +10 -2
  10. package/addon/components/cf-field/input/powerselect.hbs +2 -2
  11. package/addon/components/cf-field/input/powerselect.js +12 -0
  12. package/addon/components/cf-field/input/radio.hbs +3 -1
  13. package/addon/components/cf-field/input/radio.js +15 -0
  14. package/addon/components/cf-field/input/table.hbs +31 -1
  15. package/addon/components/cf-field/input/table.js +10 -0
  16. package/addon/components/cf-field/input/text.hbs +21 -14
  17. package/addon/components/cf-field/input/textarea.hbs +21 -14
  18. package/addon/components/cf-field/input-compare/changes-note.hbs +9 -0
  19. package/addon/components/cf-field/input-compare/text-diff.hbs +6 -0
  20. package/addon/components/cf-field/input-compare/textarea-diff.hbs +6 -0
  21. package/addon/components/cf-field/input-compare.hbs +50 -0
  22. package/addon/components/cf-field/input-compare.js +60 -0
  23. package/addon/components/cf-field/input.hbs +1 -0
  24. package/addon/components/cf-field.hbs +17 -8
  25. package/addon/components/cf-field.js +15 -3
  26. package/addon/components/cf-form-wrapper.hbs +1 -0
  27. package/addon/components/cf-form.hbs +1 -0
  28. package/addon/components/cf-navigation-item.hbs +14 -4
  29. package/addon/components/cf-navigation.hbs +1 -0
  30. package/addon/components/timeline-select.hbs +20 -0
  31. package/addon/gql/queries/document-answers-compare.graphql +157 -0
  32. package/addon/helpers/get-widget.js +17 -31
  33. package/addon/instance-initializers/form-widget-overrides.js +6 -0
  34. package/addon/lib/answer.js +38 -3
  35. package/addon/lib/compare.js +161 -0
  36. package/addon/lib/document.js +10 -2
  37. package/addon/lib/field.js +81 -1
  38. package/addon/lib/fieldset.js +11 -0
  39. package/addon/lib/navigation.js +51 -1
  40. package/addon/lib/parsers.js +57 -5
  41. package/addon/services/caluma-store.js +12 -0
  42. package/app/components/cf-field/input-compare/changes-note.js +1 -0
  43. package/app/components/cf-field/input-compare/text-diff.js +1 -0
  44. package/app/components/cf-field/input-compare/textarea-diff.js +1 -0
  45. package/app/components/cf-field/input-compare.js +1 -0
  46. package/app/components/timeline-select.js +1 -0
  47. package/app/styles/@projectcaluma/ember-form.scss +1 -0
  48. package/app/styles/_cf-content-compare.scss +214 -0
  49. package/app/styles/_cf-navigation.scss +1 -0
  50. package/package.json +5 -5
  51. package/translations/de.yaml +9 -0
  52. package/translations/en.yaml +9 -0
  53. package/translations/fr.yaml +9 -0
  54. package/translations/it.yaml +9 -0
@@ -0,0 +1,50 @@
1
+ {{#let
2
+ (component
3
+ (ensure-safe-component (get-widget @field.question))
4
+ field=@field
5
+ disabled=true
6
+ context=@context
7
+ onSave=(perform this.save)
8
+ )
9
+ as |FieldComponent|
10
+ }}
11
+ {{#if @field.isModified}}
12
+ <div
13
+ class="cf-compare cf-compare-{{@field.question.raw.__typename}}
14
+ {{if (eq @field.answer.raw.historyType '-') 'cf-compare-removed' ''}}
15
+ {{if (eq @field.answer.raw.historyType '+') 'cf-compare-added' ''}}
16
+ {{if (eq @field.answer.raw.historyType '~') 'cf-compare-modified' ''}}
17
+ "
18
+ >
19
+ {{#if this.compareOptions.combined}}
20
+ <div class="combined-diff">
21
+ <div hidden>
22
+ <FieldComponent @compare={{false}} />
23
+ </div>
24
+ <div class="diff-version">
25
+ <FieldComponent @compare={{@compare}} />
26
+ </div>
27
+ </div>
28
+ {{#unless this.compareOptions.disableChangesNote}}
29
+ <div class="changes-note">
30
+ <CfField::InputCompare::ChangesNote @field={{@field}} />
31
+ </div>
32
+ {{/unless}}
33
+ {{else}}
34
+ <div class="from-version">
35
+ <FieldComponent @compare={{@compare}} />
36
+ </div>
37
+ <div class="to-version">
38
+ <FieldComponent @compare={{false}} />
39
+ </div>
40
+ {{#unless this.compareOptions.disableChangesNote}}
41
+ <div class="changes-note">
42
+ <CfField::InputCompare::ChangesNote @field={{@field}} />
43
+ </div>
44
+ {{/unless}}
45
+ {{/if}}
46
+ </div>
47
+ {{else}}
48
+ <FieldComponent @compare={{false}} />
49
+ {{/if}}
50
+ {{/let}}
@@ -0,0 +1,60 @@
1
+ import { service } from "@ember/service";
2
+ import Component from "@glimmer/component";
3
+ import { task } from "ember-concurrency";
4
+
5
+ import { parseWidgetType } from "@projectcaluma/ember-form/lib/parsers";
6
+
7
+ /**
8
+ * Component for wrapping the compare input components
9
+ *
10
+ * @class CfFieldInputCompareComponent
11
+ */
12
+ export default class CfFieldInputCompareComponent extends Component {
13
+ @service calumaOptions;
14
+
15
+ /**
16
+ * The compare input options.
17
+ *
18
+ * @property {} compareOptions
19
+ * @accessor
20
+ */
21
+ get compareOptions() {
22
+ const { widget } = parseWidgetType(this.calumaOptions, [
23
+ this.args.field.question,
24
+ ]);
25
+
26
+ const override = this.calumaOptions
27
+ .getComponentOverrides()
28
+ .find(({ component }) => component === widget);
29
+
30
+ if (widget === "cf-field/input") {
31
+ const typeName = this.args.field?.question?.raw?.__typename;
32
+ const compareOptions = {
33
+ TextQuestion: { combined: true },
34
+ IntegerQuestion: { combined: true },
35
+ MultipleChoiceQuestion: { combined: true },
36
+ ChoiceQuestion: { combined: true },
37
+ DateQuestion: { combined: true },
38
+ TextareaQuestion: { combined: true },
39
+ TableQuestion: { combined: true, disableChangesNote: true },
40
+ };
41
+
42
+ if (typeName in compareOptions) {
43
+ return compareOptions[typeName];
44
+ }
45
+ }
46
+
47
+ return override?.compareOptions;
48
+ }
49
+
50
+ /**
51
+ * In comparison mode, never perform a save, but keep a method
52
+ * to bind to the components.
53
+ *
54
+ * @method save
55
+ * @param {String|Number|String[]} value The new value to save to the field
56
+ */
57
+ save = task({ restartable: true }, async (value) => {
58
+ return value;
59
+ });
60
+ }
@@ -4,5 +4,6 @@
4
4
  @disabled={{@disabled}}
5
5
  @onSave={{@onSave}}
6
6
  @context={{@context}}
7
+ @compare={{@compare}}
7
8
  />
8
9
  </div>
@@ -18,17 +18,26 @@
18
18
 
19
19
  <div class="uk-flex">
20
20
  <div class="uk-width-expand">
21
- {{#let
22
- (component (ensure-safe-component (get-widget @field.question)))
23
- as |FieldComponent|
24
- }}
25
- <FieldComponent
21
+ {{#if @compare}}
22
+ <CfField::InputCompare
26
23
  @field={{@field}}
27
- @disabled={{or @disabled @field.refreshAnswer.isRunning}}
24
+ @disabled={{true}}
28
25
  @context={{@context}}
29
- @onSave={{perform this.save}}
26
+ @compare={{@compare}}
30
27
  />
31
- {{/let}}
28
+ {{else}}
29
+ {{#let
30
+ (component (ensure-safe-component (get-widget @field.question)))
31
+ as |FieldComponent|
32
+ }}
33
+ <FieldComponent
34
+ @field={{@field}}
35
+ @disabled={{or @disabled @field.refreshAnswer.isRunning}}
36
+ @context={{@context}}
37
+ @onSave={{perform this.save}}
38
+ />
39
+ {{/let}}
40
+ {{/if}}
32
41
  </div>
33
42
 
34
43
  {{#if (and @field.question.raw.infoText this.infoTextVisible)}}
@@ -1,8 +1,8 @@
1
1
  import { action } from "@ember/object";
2
2
  import { service } from "@ember/service";
3
- import { macroCondition, isTesting } from "@embroider/macros";
3
+ import { isTesting, macroCondition } from "@embroider/macros";
4
4
  import Component from "@glimmer/component";
5
- import { timeout, task } from "ember-concurrency";
5
+ import { task, timeout } from "ember-concurrency";
6
6
 
7
7
  import { hasQuestionType } from "@projectcaluma/ember-core/helpers/has-question-type";
8
8
 
@@ -31,6 +31,10 @@ export default class CfFieldComponent extends Component {
31
31
  this.args.field._components.delete(this);
32
32
  }
33
33
 
34
+ get compare() {
35
+ return this.args.compare;
36
+ }
37
+
34
38
  get hasHiddenWidget() {
35
39
  return (
36
40
  this.args.field?.question.raw.meta.widgetOverride ===
@@ -77,7 +81,10 @@ export default class CfFieldComponent extends Component {
77
81
  }
78
82
 
79
83
  get saveIndicatorVisible() {
80
- return !hasQuestionType(this.args.field?.question, "action-button");
84
+ return (
85
+ !this.compare &&
86
+ !hasQuestionType(this.args.field?.question, "action-button")
87
+ );
81
88
  }
82
89
 
83
90
  /**
@@ -89,6 +96,11 @@ export default class CfFieldComponent extends Component {
89
96
  * @param {String|Number|String[]} value The new value to save to the field
90
97
  */
91
98
  save = task({ restartable: true }, async (value) => {
99
+ // Do not save when in comparison mode.
100
+ if (this.compare) {
101
+ return;
102
+ }
103
+
92
104
  if (typeof this.args.onSave === "function") {
93
105
  return await this.args.onSave(this.args.field, value);
94
106
  }
@@ -10,6 +10,7 @@
10
10
  @document={{@document}}
11
11
  @fieldset={{@fieldset}}
12
12
  @context={{@context}}
13
+ @compare={{@compare}}
13
14
  @disabled={{@disabled}}
14
15
  @onSave={{@onSave}}
15
16
  />
@@ -7,6 +7,7 @@
7
7
  @field={{field}}
8
8
  @disabled={{@disabled}}
9
9
  @context={{@context}}
10
+ @compare={{@compare}}
10
11
  @onSave={{@onSave}}
11
12
  />
12
13
  {{/if}}
@@ -17,10 +17,19 @@
17
17
  {{@item.label}}
18
18
  </span>
19
19
  {{/if}}
20
- <span
21
- class="cf-navigation__item__icon cf-navigation__item__icon--{{@item.state}}
22
- {{if @item.dirty 'cf-navigation__item__icon--dirty'}}"
23
- ></span>
20
+ {{#if @compare}}
21
+ {{#if (eq @item.compareState "modified")}}
22
+ <UkIcon
23
+ @icon="pencil"
24
+ class="cf-navigation__item__icon cf-navigation__item__icon--compare-{{@item.compareState}}"
25
+ />
26
+ {{/if}}
27
+ {{else}}
28
+ <span
29
+ class="cf-navigation__item__icon cf-navigation__item__icon--{{@item.state}}
30
+ {{if @item.dirty 'cf-navigation__item__icon--dirty'}}"
31
+ ></span>
32
+ {{/if}}
24
33
  </LinkTo>
25
34
 
26
35
  {{#if @item.visibleChildren}}
@@ -29,6 +38,7 @@
29
38
  <CfNavigationItem
30
39
  @item={{child}}
31
40
  @headingLevel={{add @headingLevel 1}}
41
+ @compare={{@compare}}
32
42
  />
33
43
  {{/each}}
34
44
  </ul>
@@ -7,6 +7,7 @@
7
7
  @item={{item}}
8
8
  @useAsHeading={{@useAsHeading}}
9
9
  @headingLevel={{@headingBaseLevel}}
10
+ @compare={{@compare}}
10
11
  />
11
12
  {{#unless item.fieldset.field}}
12
13
  <hr class="uk-divider-small uk-margin-small" />
@@ -0,0 +1,20 @@
1
+ <PowerSelect
2
+ class="uk-margin-bottom"
3
+ @selected={{@selected}}
4
+ @options={{@options}}
5
+ @searchEnabled={{false}}
6
+ @searchField="label"
7
+ @allowClear={{true}}
8
+ @onChange={{@onChange}}
9
+ as |option|
10
+ >
11
+ {{#if (eq option.id "current")}}
12
+ {{t "caluma.form.compare.timeline.current"}}
13
+ {{else}}
14
+ {{option.label}}:
15
+ {{format-date option.startDate}}
16
+ {{#if option.endDate}}
17
+ -
18
+ {{format-date option.endDate}}{{/if}}
19
+ {{/if}}
20
+ </PowerSelect>
@@ -0,0 +1,157 @@
1
+ #import FieldQuestion, SimpleQuestion, FieldTableQuestion from '../fragments/field.graphql'
2
+
3
+ fragment SimpleRevisionAnswer on HistoricalAnswer {
4
+ id
5
+ historyDate
6
+ historyUserId
7
+ historyType
8
+ question {
9
+ slug
10
+ label
11
+ ... on ChoiceQuestion {
12
+ choiceOptions: options {
13
+ edges {
14
+ node {
15
+ slug
16
+ label
17
+ }
18
+ }
19
+ }
20
+ }
21
+ ... on DynamicChoiceQuestion {
22
+ dynamicChoiceOptions: options(context: $context) {
23
+ edges {
24
+ node {
25
+ slug
26
+ label
27
+ }
28
+ }
29
+ }
30
+ }
31
+ ... on MultipleChoiceQuestion {
32
+ multipleChoiceOptions: options {
33
+ edges {
34
+ node {
35
+ slug
36
+ label
37
+ }
38
+ }
39
+ }
40
+ }
41
+ ... on DynamicMultipleChoiceQuestion {
42
+ dynamicMultipleChoiceOptions: options(context: $context) {
43
+ edges {
44
+ node {
45
+ slug
46
+ label
47
+ }
48
+ }
49
+ }
50
+ }
51
+ __typename
52
+ }
53
+ ... on HistoricalStringAnswer {
54
+ historyId
55
+ stringValue: value
56
+ }
57
+ ... on HistoricalListAnswer {
58
+ historyId
59
+ listValue: value
60
+ }
61
+ ... on HistoricalIntegerAnswer {
62
+ historyId
63
+ integerValue: value
64
+ }
65
+ ... on HistoricalFloatAnswer {
66
+ historyId
67
+ floatValue: value
68
+ }
69
+ ... on HistoricalDateAnswer {
70
+ historyId
71
+ dateValue: value
72
+ }
73
+ __typename
74
+ }
75
+
76
+ fragment HistoricalTableValue on HistoricalDocument {
77
+ id: documentId
78
+ documentId: id
79
+ historyDate
80
+ historyType
81
+ historyUserId
82
+ form {
83
+ id
84
+ slug
85
+ questions {
86
+ edges {
87
+ node {
88
+ ...FieldQuestion
89
+ }
90
+ }
91
+ }
92
+ }
93
+ __typename
94
+ }
95
+
96
+ fragment RevisionDocument on HistoricalDocument {
97
+ historicalDocumentId: id
98
+ id: documentId
99
+ form {
100
+ id
101
+ slug
102
+ }
103
+ }
104
+
105
+ query (
106
+ $documentId: ID!
107
+ $from: DateTime!
108
+ $to: DateTime!
109
+ $context: JSONString
110
+ ) {
111
+ fromRevision: documentAsOf(id: $documentId, asOf: $from) {
112
+ ...RevisionDocument
113
+ answers: historicalAnswers(asOf: $from, excludeDeleted: true) {
114
+ edges {
115
+ node {
116
+ ...SimpleRevisionAnswer
117
+ ... on HistoricalTableAnswer {
118
+ tableValue: value(asOf: $from, excludeDeleted: true) {
119
+ ...HistoricalTableValue
120
+
121
+ answers: historicalAnswers(asOf: $from, excludeDeleted: true) {
122
+ edges {
123
+ node {
124
+ ...SimpleRevisionAnswer
125
+ }
126
+ }
127
+ }
128
+ }
129
+ }
130
+ }
131
+ }
132
+ }
133
+ }
134
+ toRevision: documentAsOf(id: $documentId, asOf: $to) {
135
+ ...RevisionDocument
136
+ answers: historicalAnswers(asOf: $to) {
137
+ edges {
138
+ node {
139
+ ...SimpleRevisionAnswer
140
+ ... on HistoricalTableAnswer {
141
+ tableValue: value(asOf: $to) {
142
+ ...HistoricalTableValue
143
+
144
+ answers: historicalAnswers(asOf: $to) {
145
+ edges {
146
+ node {
147
+ ...SimpleRevisionAnswer
148
+ }
149
+ }
150
+ }
151
+ }
152
+ }
153
+ }
154
+ }
155
+ }
156
+ }
157
+ }
@@ -1,10 +1,10 @@
1
1
  import Helper from "@ember/component/helper";
2
- import { warn } from "@ember/debug";
3
2
  import { inject as service } from "@ember/service";
4
3
  import { ensureSafeComponent } from "@embroider/util";
5
4
 
6
5
  import InputComponent from "@projectcaluma/ember-form/components/cf-field/input";
7
6
  import FormComponent from "@projectcaluma/ember-form/components/cf-form";
7
+ import { parseWidgetType } from "@projectcaluma/ember-form/lib/parsers";
8
8
 
9
9
  const DEFAULT_WIDGETS = {
10
10
  "cf-field/input": InputComponent,
@@ -33,38 +33,24 @@ const DEFAULT_WIDGETS = {
33
33
  export default class GetWidgetHelper extends Helper {
34
34
  @service calumaOptions;
35
35
 
36
- compute(params, { default: defaultWidget = "cf-field/input" }) {
37
- for (const obj of params) {
38
- let widget = obj?.raw?.meta?.widgetOverride;
39
-
40
- if (obj?.useNumberSeparatorWidget) {
41
- widget = "cf-field/input/number-separator";
42
- }
43
-
44
- if (!widget) {
45
- continue;
46
- }
47
-
48
- const override =
49
- widget &&
50
- this.calumaOptions
51
- .getComponentOverrides()
52
- .find(({ component }) => component === widget);
53
-
54
- warn(
55
- `Widget override "${widget}" is not registered. Please register it by calling \`calumaOptions.registerComponentOverride\``,
56
- override,
57
- { id: "ember-caluma.unregistered-override" },
36
+ compute(params, options = {}) {
37
+ const { widget, override } = parseWidgetType(
38
+ this.calumaOptions,
39
+ params,
40
+ options,
41
+ );
42
+
43
+ if (override) {
44
+ const overrideWidget = this.calumaOptions
45
+ .getComponentOverrides()
46
+ .find(({ component }) => component === widget);
47
+
48
+ return ensureSafeComponent(
49
+ overrideWidget.componentClass ?? overrideWidget.component,
50
+ this,
58
51
  );
59
-
60
- if (override) {
61
- return ensureSafeComponent(
62
- override.componentClass ?? override.component,
63
- this,
64
- );
65
- }
66
52
  }
67
53
 
68
- return ensureSafeComponent(DEFAULT_WIDGETS[defaultWidget], this);
54
+ return ensureSafeComponent(DEFAULT_WIDGETS[widget], this);
69
55
  }
70
56
  }
@@ -33,6 +33,9 @@ class PowerSelectOverride {
33
33
  "DynamicChoiceQuestion",
34
34
  "DynamicMultipleChoiceQuestion",
35
35
  ];
36
+ compareOptions = {
37
+ combined: false,
38
+ };
36
39
  }
37
40
 
38
41
  class NumberSeparatorOverride {
@@ -47,6 +50,9 @@ class NumberSeparatorOverride {
47
50
  component = "cf-field/input/number-separator";
48
51
  componentClass = NumberSeparatorComponent;
49
52
  types = ["IntegerQuestion", "FloatQuestion", "CalculatedFloatQuestion"];
53
+ compareOptions = {
54
+ combined: true,
55
+ };
50
56
  }
51
57
 
52
58
  export function initialize(appInstance) {
@@ -2,10 +2,11 @@ import { getOwner } from "@ember/application";
2
2
  import { assert } from "@ember/debug";
3
3
  import { camelize } from "@ember/string";
4
4
  import { isEmpty } from "@ember/utils";
5
- import { dedupeTracked, cached } from "tracked-toolbox";
5
+ import { cached, dedupeTracked } from "tracked-toolbox";
6
6
 
7
7
  import { decodeId } from "@projectcaluma/ember-core/helpers/decode-id";
8
8
  import Base from "@projectcaluma/ember-form/lib/base";
9
+ import { historicalTableValue } from "@projectcaluma/ember-form/lib/compare";
9
10
  import { parseDocument } from "@projectcaluma/ember-form/lib/parsers";
10
11
 
11
12
  /**
@@ -33,7 +34,7 @@ class DedupedTrackedObject {
33
34
  * @class Answer
34
35
  */
35
36
  export default class Answer extends Base {
36
- constructor({ raw, field, ...args }) {
37
+ constructor({ raw, field, historical, ...args }) {
37
38
  assert("`field` must be passed as an argument", field);
38
39
 
39
40
  assert(
@@ -45,6 +46,7 @@ export default class Answer extends Base {
45
46
 
46
47
  this.field = field;
47
48
  this.raw = new DedupedTrackedObject(raw);
49
+ this.historical = historical ? new DedupedTrackedObject(historical) : null;
48
50
 
49
51
  this.pushIntoStore();
50
52
  }
@@ -64,6 +66,14 @@ export default class Answer extends Base {
64
66
  */
65
67
  raw = {};
66
68
 
69
+ /**
70
+ * Get the compare context via the field.
71
+ * @property {Object} compare
72
+ */
73
+ get compare() {
74
+ return this.field.compare;
75
+ }
76
+
67
77
  /**
68
78
  * The primary key of the answer.
69
79
  *
@@ -106,7 +116,11 @@ export default class Answer extends Base {
106
116
  get _valueKey() {
107
117
  return (
108
118
  this.raw.__typename &&
109
- camelize(this.raw.__typename.replace(/Answer$/, "Value"))
119
+ camelize(
120
+ this.raw.__typename
121
+ .replace(/^Historical/, "")
122
+ .replace(/Answer$/, "Value"),
123
+ )
110
124
  );
111
125
  }
112
126
 
@@ -121,6 +135,15 @@ export default class Answer extends Base {
121
135
  const value = this.raw[this._valueKey];
122
136
 
123
137
  if (this._valueKey === "tableValue" && value) {
138
+ // For a historical view for table values we map it differently to be able to
139
+ // show the diff.
140
+ if (this.compare) {
141
+ const owner = getOwner(this);
142
+ const historicalValue = this.historical?.[this._valueKey] || [];
143
+
144
+ return historicalTableValue(owner, this.field, value, historicalValue);
145
+ }
146
+
124
147
  const owner = getOwner(this);
125
148
  const Document = owner.factoryFor("caluma-model:document").class;
126
149
 
@@ -146,6 +169,18 @@ export default class Answer extends Base {
146
169
  return value;
147
170
  }
148
171
 
172
+ get historicalValue() {
173
+ return this.historical?.[this._valueKey];
174
+ }
175
+
176
+ get historicalDate() {
177
+ return this.historical?.historyDate;
178
+ }
179
+
180
+ get historicalUser() {
181
+ return this.historical?.historyUserId;
182
+ }
183
+
149
184
  set value(value) {
150
185
  if (this._valueKey) {
151
186
  this.raw[this._valueKey] = [undefined, ""].includes(value) ? null : value;