@projectcaluma/ember-form 11.0.0-beta.21 → 11.0.0-beta.22

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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,14 @@
1
+ # [@projectcaluma/ember-form-v11.0.0-beta.22](https://github.com/projectcaluma/ember-caluma/compare/@projectcaluma/ember-form-v11.0.0-beta.21...@projectcaluma/ember-form-v11.0.0-beta.22) (2022-08-05)
2
+
3
+
4
+ * feat!: add multi file upload (#2040) ([c4fd004](https://github.com/projectcaluma/ember-caluma/commit/c4fd0049654b2d2e5ea62e5909a45d89cb888b40)), closes [#2040](https://github.com/projectcaluma/ember-caluma/issues/2040)
5
+
6
+
7
+ ### BREAKING CHANGES
8
+
9
+ * This requires the caluma backend version v8.0.0-beta.12
10
+ or later.
11
+
1
12
  # [@projectcaluma/ember-form-v11.0.0-beta.21](https://github.com/projectcaluma/ember-caluma/compare/@projectcaluma/ember-form-v11.0.0-beta.20...@projectcaluma/ember-form-v11.0.0-beta.21) (2022-06-09)
2
13
 
3
14
 
@@ -0,0 +1,35 @@
1
+ <div class="uk-flex-middle uk-grid-divider uk-grid-column-small" uk-grid>
2
+ <div uk-form-custom="target: true">
3
+
4
+ <input
5
+ type="file"
6
+ name={{@field.pk}}
7
+ id={{@field.pk}}
8
+ disabled={{@disabled}}
9
+ multiple
10
+ {{on "change" this.save}}
11
+ />
12
+ <UkButton disabled={{@disabled}}>
13
+ {{t "caluma.form.selectFile"}}
14
+ </UkButton>
15
+ </div>
16
+ <ul class="uk-list uk-list-collapse" data-test-file-list={{@field.pk}}>
17
+ {{#each this.files as |file|}}
18
+ <li class="uk-text-justify uk-text-middle">
19
+ <UkButton
20
+ data-test-download-link={{file.id}}
21
+ @color="link"
22
+ @onClick={{fn this.download file.id}}
23
+ >
24
+ {{file.name}}
25
+ </UkButton>
26
+ <UkIcon
27
+ class="uk-icon-button uk-margin-small-left"
28
+ role="button"
29
+ @icon="trash"
30
+ {{on "click" (fn this.delete file.id)}}
31
+ />
32
+ </li>
33
+ {{/each}}
34
+ </ul>
35
+ </div>
@@ -0,0 +1,113 @@
1
+ import { action } from "@ember/object";
2
+ import { inject as service } from "@ember/service";
3
+ import { macroCondition, isTesting } from "@embroider/macros";
4
+ import Component from "@glimmer/component";
5
+ import { queryManager } from "ember-apollo-client";
6
+ import fetch from "fetch";
7
+
8
+ import getFilesAnswerInfoQuery from "@projectcaluma/ember-form/gql/queries/filesanswer-info.graphql";
9
+
10
+ export default class CfFieldInputFilesComponent extends Component {
11
+ @service intl;
12
+
13
+ @queryManager apollo;
14
+
15
+ get files() {
16
+ return this.args.field?.answer?.value;
17
+ }
18
+
19
+ @action
20
+ async download(fileId) {
21
+ if (!fileId) {
22
+ return;
23
+ }
24
+ const answers = await this.apollo.query(
25
+ {
26
+ query: getFilesAnswerInfoQuery,
27
+ variables: { id: this.args.field.answer.raw.id },
28
+ fetchPolicy: "network-only",
29
+ },
30
+ "node.value"
31
+ );
32
+ const { downloadUrl } =
33
+ answers.find((file) =>
34
+ // the testing graph-ql setup does a base64 encoding of `__typename: fileID`
35
+ macroCondition(isTesting())
36
+ ? file.id === fileId ||
37
+ atob(file.id).substring(file.__typename.length + 1) === fileId
38
+ : file.id === fileId
39
+ ) ?? {};
40
+ if (downloadUrl) {
41
+ window.open(downloadUrl, "_blank");
42
+ }
43
+ }
44
+
45
+ @action
46
+ async save({ target }) {
47
+ // store the old list of files
48
+ // unwrap files from FileList construct
49
+ let newFiles = Array.from(target.files).map((file) => ({
50
+ name: file.name,
51
+ value: file,
52
+ }));
53
+
54
+ const fileList = [...(this.files || []), ...newFiles];
55
+
56
+ if (newFiles.length === 0) {
57
+ return;
58
+ }
59
+
60
+ // trigger save action for file list of old and new files with
61
+ // reduces properties to match gql format
62
+ const { filesValue: savedAnswerValue } = await this.args.onSave(
63
+ fileList.map(({ name, id }) => ({ name, id }))
64
+ );
65
+
66
+ try {
67
+ // iterate over list of new files and enrich with graphql answer values
68
+ newFiles = newFiles.map((file) => ({
69
+ ...savedAnswerValue.find(
70
+ (value) =>
71
+ file.name === value.name &&
72
+ !fileList.find((file) => file.id === value.id)
73
+ ),
74
+ value: file.value,
75
+ }));
76
+
77
+ const uploadFunction = async (file) => {
78
+ const response = await fetch(file.uploadUrl, {
79
+ method: "PUT",
80
+ body: file.value,
81
+ });
82
+ if (!response.ok) {
83
+ throw new Error();
84
+ }
85
+ return response;
86
+ };
87
+
88
+ // upload the actual file to data storage
89
+ await Promise.all(newFiles.map((file) => uploadFunction(file)));
90
+
91
+ this.args.field.answer.value = savedAnswerValue;
92
+ } catch (error) {
93
+ await this.args.onSave([]);
94
+ this.args.field._errors = [{ type: "uploadFailed" }];
95
+ } finally {
96
+ // eslint-disable-next-line require-atomic-updates
97
+ target.value = "";
98
+ }
99
+ }
100
+
101
+ @action
102
+ async delete(fileId) {
103
+ const remainingFiles = this.files
104
+ .filter((file) => file.id !== fileId)
105
+ .map(({ name, id }) => ({ name, id }));
106
+
107
+ try {
108
+ await this.args.onSave(remainingFiles);
109
+ } catch (error) {
110
+ this.args.field._errors = [{ type: "deleteFailed" }];
111
+ }
112
+ }
113
+ }
@@ -4,7 +4,7 @@ import Component from "@glimmer/component";
4
4
  import ActionButtonComponent from "@projectcaluma/ember-form/components/cf-field/input/action-button";
5
5
  import CheckboxComponent from "@projectcaluma/ember-form/components/cf-field/input/checkbox";
6
6
  import DateComponent from "@projectcaluma/ember-form/components/cf-field/input/date";
7
- import FileComponent from "@projectcaluma/ember-form/components/cf-field/input/file";
7
+ import FilesComponent from "@projectcaluma/ember-form/components/cf-field/input/files";
8
8
  import FloatComponent from "@projectcaluma/ember-form/components/cf-field/input/float";
9
9
  import IntegerComponent from "@projectcaluma/ember-form/components/cf-field/input/integer";
10
10
  import RadioComponent from "@projectcaluma/ember-form/components/cf-field/input/radio";
@@ -20,7 +20,7 @@ const COMPONENT_MAPPING = {
20
20
  DateQuestion: DateComponent,
21
21
  DynamicChoiceQuestion: RadioComponent,
22
22
  DynamicMultipleChoiceQuestion: CheckboxComponent,
23
- FileQuestion: FileComponent,
23
+ FilesQuestion: FilesComponent,
24
24
  FloatQuestion: FloatComponent,
25
25
  IntegerQuestion: IntegerComponent,
26
26
  MultipleChoiceQuestion: CheckboxComponent,
@@ -11,14 +11,14 @@
11
11
  month="2-digit"
12
12
  year="numeric"
13
13
  }}
14
- {{else if (has-question-type @field.question "file")}}
15
- {{#if @field.answer.value}}
14
+ {{else if (has-question-type @field.question "files")}}
15
+ {{#each @field.answer.value as |file|}}
16
16
  <UkButton
17
17
  @color="link"
18
- @label={{@field.answer.value.name}}
19
- @onClick={{fn this.download @field.answer.raw.id}}
18
+ @label={{file.name}}
19
+ @onClick={{fn this.download file.id}}
20
20
  />
21
- {{/if}}
21
+ {{/each}}
22
22
  {{else}}
23
23
  {{@field.answer.value}}
24
24
  {{/if}}
@@ -2,22 +2,23 @@ import { action } from "@ember/object";
2
2
  import Component from "@glimmer/component";
3
3
  import { queryManager } from "ember-apollo-client";
4
4
 
5
- import getFileAnswerInfoQuery from "@projectcaluma/ember-form/gql/queries/fileanswer-info.graphql";
5
+ import getFilesAnswerInfoQuery from "@projectcaluma/ember-form/gql/queries/filesanswer-info.graphql";
6
6
 
7
7
  export default class CfFieldValueComponent extends Component {
8
8
  @queryManager apollo;
9
9
 
10
10
  @action
11
11
  async download(id) {
12
- const { downloadUrl } = await this.apollo.query(
12
+ const files = await this.apollo.query(
13
13
  {
14
- query: getFileAnswerInfoQuery,
15
- variables: { id },
14
+ query: getFilesAnswerInfoQuery,
15
+ variables: { id: this.args.field.answer.raw.id },
16
16
  fetchPolicy: "network-only",
17
17
  },
18
- "node.fileValue"
18
+ "node.value"
19
19
  );
20
20
 
21
+ const { downloadUrl } = files?.find((file) => file.id === id);
21
22
  if (downloadUrl) {
22
23
  window.open(downloadUrl, "_blank");
23
24
  }
@@ -117,7 +117,7 @@ fragment SimpleQuestion on Question {
117
117
  calcExpression
118
118
  hintText
119
119
  }
120
- ... on FileQuestion {
120
+ ... on FilesQuestion {
121
121
  hintText
122
122
  }
123
123
  ... on ActionButtonQuestion {
@@ -234,8 +234,8 @@ fragment SimpleAnswer on Answer {
234
234
  ... on ListAnswer {
235
235
  listValue: value
236
236
  }
237
- ... on FileAnswer {
238
- fileValue: value {
237
+ ... on FilesAnswer {
238
+ filesValue: value {
239
239
  id
240
240
  uploadUrl
241
241
  downloadUrl
@@ -0,0 +1,9 @@
1
+ #import * from '../fragments/field.graphql'
2
+
3
+ mutation SaveDocumentFilesAnswer($input: SaveDocumentFilesAnswerInput!) {
4
+ saveDocumentFilesAnswer(input: $input) {
5
+ answer {
6
+ ...FieldAnswer
7
+ }
8
+ }
9
+ }
@@ -1,8 +1,8 @@
1
- query FileAnswerInfo($id: ID!) {
1
+ query FilesAnswerInfo($id: ID!) {
2
2
  node(id: $id) {
3
- id
4
- ... on FileAnswer {
5
- fileValue: value {
3
+ ... on FilesAnswer {
4
+ id
5
+ value {
6
6
  id
7
7
  uploadUrl
8
8
  downloadUrl
@@ -13,7 +13,7 @@ import { cached } from "tracked-toolbox";
13
13
 
14
14
  import { decodeId } from "@projectcaluma/ember-core/helpers/decode-id";
15
15
  import saveDocumentDateAnswerMutation from "@projectcaluma/ember-form/gql/mutations/save-document-date-answer.graphql";
16
- import saveDocumentFileAnswerMutation from "@projectcaluma/ember-form/gql/mutations/save-document-file-answer.graphql";
16
+ import saveDocumentFilesAnswerMutation from "@projectcaluma/ember-form/gql/mutations/save-document-files-answer.graphql";
17
17
  import saveDocumentFloatAnswerMutation from "@projectcaluma/ember-form/gql/mutations/save-document-float-answer.graphql";
18
18
  import saveDocumentIntegerAnswerMutation from "@projectcaluma/ember-form/gql/mutations/save-document-integer-answer.graphql";
19
19
  import saveDocumentListAnswerMutation from "@projectcaluma/ember-form/gql/mutations/save-document-list-answer.graphql";
@@ -34,7 +34,7 @@ export const TYPE_MAP = {
34
34
  DynamicChoiceQuestion: "StringAnswer",
35
35
  TableQuestion: "TableAnswer",
36
36
  FormQuestion: null,
37
- FileQuestion: "FileAnswer",
37
+ FilesQuestion: "FilesAnswer",
38
38
  StaticQuestion: null,
39
39
  DateQuestion: "DateAnswer",
40
40
  };
@@ -44,7 +44,7 @@ const MUTATION_MAP = {
44
44
  IntegerAnswer: saveDocumentIntegerAnswerMutation,
45
45
  StringAnswer: saveDocumentStringAnswerMutation,
46
46
  ListAnswer: saveDocumentListAnswerMutation,
47
- FileAnswer: saveDocumentFileAnswerMutation,
47
+ FilesAnswer: saveDocumentFilesAnswerMutation,
48
48
  DateAnswer: saveDocumentDateAnswerMutation,
49
49
  TableAnswer: saveDocumentTableAnswerMutation,
50
50
  };
@@ -810,11 +810,11 @@ export default class Field extends Base {
810
810
  /**
811
811
  * Dummy method for the validation of file uploads.
812
812
  *
813
- * @method _validateFileQuestion
813
+ * @method _validateFilesQuestion
814
814
  * @return {Boolean} Always returns true
815
815
  * @private
816
816
  */
817
- _validateFileQuestion() {
817
+ _validateFilesQuestion() {
818
818
  return true;
819
819
  }
820
820
 
@@ -1 +1 @@
1
- export { default } from "@projectcaluma/ember-form/components/cf-field/input/file";
1
+ export { default } from "@projectcaluma/ember-form/components/cf-field/input/files";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@projectcaluma/ember-form",
3
- "version": "11.0.0-beta.21",
3
+ "version": "11.0.0-beta.22",
4
4
  "description": "Ember addon for rendering Caluma forms.",
5
5
  "keywords": [
6
6
  "ember-addon"
@@ -15,16 +15,16 @@
15
15
  },
16
16
  "dependencies": {
17
17
  "@ember/string": "^3.0.0",
18
- "@embroider/macros": "^1.7.1",
19
- "@embroider/util": "^1.7.1",
18
+ "@embroider/macros": "^1.8.3",
19
+ "@embroider/util": "^1.8.3",
20
20
  "@glimmer/component": "^1.1.2",
21
21
  "@glimmer/tracking": "^1.1.2",
22
- "@projectcaluma/ember-core": "^11.0.0-beta.7",
23
- "ember-apollo-client": "^4.0.2",
22
+ "@projectcaluma/ember-core": "^11.0.0-beta.9",
23
+ "ember-apollo-client": "~4.0.2",
24
24
  "ember-auto-import": "^2.4.2",
25
25
  "ember-autoresize-modifier": "^0.5.0",
26
26
  "ember-cli-babel": "^7.26.11",
27
- "ember-cli-htmlbars": "^6.0.1",
27
+ "ember-cli-htmlbars": "^6.1.0",
28
28
  "ember-cli-showdown": "^6.0.1",
29
29
  "ember-composable-helpers": "^5.0.0",
30
30
  "ember-concurrency": "^2.2.1",
@@ -34,21 +34,21 @@
34
34
  "ember-math-helpers": "^2.18.2",
35
35
  "ember-pikaday": "^4.0.0",
36
36
  "ember-power-select": "^5.0.4",
37
- "ember-resources": "^4.8.2",
37
+ "ember-resources": "^5.1.1",
38
38
  "ember-uikit": "^5.1.3",
39
39
  "ember-validators": "^4.1.2",
40
40
  "graphql": "^15.8.0",
41
41
  "jexl": "^2.3.0",
42
42
  "lodash.isequal": "^4.5.0",
43
- "luxon": "^2.4.0",
43
+ "luxon": "^3.0.1",
44
44
  "tracked-toolbox": "^1.2.3"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@ember/optional-features": "2.0.0",
48
48
  "@ember/test-helpers": "2.8.1",
49
- "@embroider/test-setup": "1.7.1",
50
- "@faker-js/faker": "7.2.0",
51
- "@projectcaluma/ember-testing": "11.0.0-beta.9",
49
+ "@embroider/test-setup": "1.8.3",
50
+ "@faker-js/faker": "7.3.0",
51
+ "@projectcaluma/ember-testing": "11.0.0-beta.10",
52
52
  "@projectcaluma/ember-workflow": "^11.0.0-beta.7",
53
53
  "broccoli-asset-rev": "3.0.0",
54
54
  "ember-cli": "3.28.5",
@@ -73,7 +73,7 @@
73
73
  "qunit": "2.19.1",
74
74
  "qunit-dom": "2.0.0",
75
75
  "uuid": "8.3.2",
76
- "webpack": "5.73.0"
76
+ "webpack": "5.74.0"
77
77
  },
78
78
  "peerDependencies": {
79
79
  "@projectcaluma/ember-workflow": "^11.0.0-beta.7"
@@ -1,32 +0,0 @@
1
- <div class="uk-flex-middle uk-grid-divider uk-grid-column-small" uk-grid>
2
- <div uk-form-custom="target: true">
3
-
4
- <input
5
- type="file"
6
- name={{@field.pk}}
7
- id={{@field.pk}}
8
- disabled={{@disabled}}
9
- {{on "change" this.save}}
10
- />
11
- <UkButton disabled={{@disabled}}>
12
- {{t "caluma.form.selectFile"}}
13
- </UkButton>
14
- </div>
15
- {{#if (and this.downloadUrl this.downloadName)}}
16
- <div>
17
- <UkButton
18
- data-test-download-link
19
- @color="link"
20
- @onClick={{this.download}}
21
- >
22
- {{this.downloadName}}
23
- </UkButton>
24
- <UkIcon
25
- class="uk-icon-button uk-margin-small-left"
26
- role="button"
27
- @icon="trash"
28
- {{on "click" this.delete}}
29
- />
30
- </div>
31
- {{/if}}
32
- </div>
@@ -1,89 +0,0 @@
1
- import { action } from "@ember/object";
2
- import { inject as service } from "@ember/service";
3
- import Component from "@glimmer/component";
4
- import { queryManager } from "ember-apollo-client";
5
- import fetch from "fetch";
6
-
7
- import removeAnswerMutation from "@projectcaluma/ember-form/gql/mutations/remove-answer.graphql";
8
- import getFileAnswerInfoQuery from "@projectcaluma/ember-form/gql/queries/fileanswer-info.graphql";
9
-
10
- export default class CfFieldInputFileComponent extends Component {
11
- @service intl;
12
-
13
- @queryManager apollo;
14
-
15
- get downloadUrl() {
16
- return this.args.field?.answer?.value?.downloadUrl;
17
- }
18
-
19
- get downloadName() {
20
- return this.args.field?.answer?.value?.name;
21
- }
22
-
23
- @action
24
- async download() {
25
- const { downloadUrl } = await this.apollo.query(
26
- {
27
- query: getFileAnswerInfoQuery,
28
- variables: { id: this.args.field.answer.raw.id },
29
- fetchPolicy: "network-only",
30
- },
31
- "node.fileValue"
32
- );
33
-
34
- if (downloadUrl) {
35
- window.open(downloadUrl, "_blank");
36
- }
37
- }
38
-
39
- @action
40
- async save({ target }) {
41
- const file = target.files[0];
42
-
43
- if (!file) {
44
- return;
45
- }
46
-
47
- const { fileValue } = await this.args.onSave(file.name);
48
-
49
- try {
50
- const response = await fetch(fileValue.uploadUrl, {
51
- method: "PUT",
52
- body: file,
53
- });
54
-
55
- if (!response.ok) {
56
- throw new Error();
57
- }
58
-
59
- this.args.field.answer.value = {
60
- name: file.name,
61
- downloadUrl: fileValue.downloadUrl,
62
- };
63
- } catch (error) {
64
- await this.args.onSave(null);
65
- this.args.field._errors = [{ type: "uploadFailed" }];
66
- } finally {
67
- // eslint-disable-next-line require-atomic-updates
68
- target.value = "";
69
- }
70
- }
71
-
72
- @action
73
- async delete() {
74
- try {
75
- await this.apollo.mutate({
76
- mutation: removeAnswerMutation,
77
- variables: {
78
- input: {
79
- answer: this.args.field.answer.uuid,
80
- },
81
- },
82
- });
83
-
84
- await this.args.onSave(null);
85
- } catch (error) {
86
- this.args.field._errors = [{ type: "deleteFailed" }];
87
- }
88
- }
89
- }
@@ -1,7 +0,0 @@
1
- mutation RemoveAnswer($input: RemoveAnswerInput!) {
2
- removeAnswer(input: $input) {
3
- answer {
4
- id
5
- }
6
- }
7
- }
@@ -1,9 +0,0 @@
1
- #import * from '../fragments/field.graphql'
2
-
3
- mutation SaveDocumentFileAnswer($input: SaveDocumentFileAnswerInput!) {
4
- saveDocumentFileAnswer(input: $input) {
5
- answer {
6
- ...FieldAnswer
7
- }
8
- }
9
- }