diff --git a/packages/core/addon/-private/possible-types.js b/packages/core/addon/-private/possible-types.js index ccf567030a..46c6a8c3a6 100644 --- a/packages/core/addon/-private/possible-types.js +++ b/packages/core/addon/-private/possible-types.js @@ -11,7 +11,7 @@ export default { "DateQuestion", "TableQuestion", "FormQuestion", - "FileQuestion", + "FilesQuestion", "StaticQuestion", "CalculatedFloatQuestion", "ActionButtonQuestion", @@ -35,7 +35,7 @@ export default { "DateQuestion", "TableQuestion", "FormQuestion", - "FileQuestion", + "FilesQuestion", "StaticQuestion", "StringAnswer", "ListAnswer", @@ -43,7 +43,7 @@ export default { "FloatAnswer", "DateAnswer", "TableAnswer", - "FileAnswer", + "FilesAnswer", "File", "CalculatedFloatQuestion", "ActionButtonQuestion", @@ -62,7 +62,7 @@ export default { "FloatAnswer", "DateAnswer", "TableAnswer", - "FileAnswer", + "FilesAnswer", ], Task: ["SimpleTask", "CompleteWorkflowFormTask", "CompleteTaskFormTask"], DynamicQuestion: ["DynamicChoiceQuestion", "DynamicMultipleChoiceQuestion"], diff --git a/packages/form-builder/addon/components/cfb-form-editor/question.js b/packages/form-builder/addon/components/cfb-form-editor/question.js index 45329ef4f9..8ce72a444d 100644 --- a/packages/form-builder/addon/components/cfb-form-editor/question.js +++ b/packages/form-builder/addon/components/cfb-form-editor/question.js @@ -26,7 +26,7 @@ import saveDefaultStringAnswerMutation from "@projectcaluma/ember-form-builder/g import saveDefaultTableAnswerMutation from "@projectcaluma/ember-form-builder/gql/mutations/save-default-table-answer.graphql"; import saveDynamicChoiceQuestionMutation from "@projectcaluma/ember-form-builder/gql/mutations/save-dynamic-choice-question.graphql"; import saveDynamicMultipleChoiceQuestionMutation from "@projectcaluma/ember-form-builder/gql/mutations/save-dynamic-multiple-choice-question.graphql"; -import saveFileQuestionMutation from "@projectcaluma/ember-form-builder/gql/mutations/save-file-question.graphql"; +import saveFilesQuestionMutation from "@projectcaluma/ember-form-builder/gql/mutations/save-files-question.graphql"; import saveFloatQuestionMutation from "@projectcaluma/ember-form-builder/gql/mutations/save-float-question.graphql"; import saveFormQuestionMutation from "@projectcaluma/ember-form-builder/gql/mutations/save-form-question.graphql"; import saveIntegerQuestionMutation from "@projectcaluma/ember-form-builder/gql/mutations/save-integer-question.graphql"; @@ -53,7 +53,7 @@ export const TYPES = { DynamicChoiceQuestion: saveDynamicChoiceQuestionMutation, TableQuestion: saveTableQuestionMutation, FormQuestion: saveFormQuestionMutation, - FileQuestion: saveFileQuestionMutation, + FilesQuestion: saveFilesQuestionMutation, StaticQuestion: saveStaticQuestionMutation, DateQuestion: saveDateQuestionMutation, CalculatedFloatQuestion: saveCalculatedFloatQuestionMutation, @@ -335,7 +335,7 @@ export default class CfbFormEditorQuestion extends Component { }; } - _getFileQuestionInput(changeset) { + _getFilesQuestionInput(changeset) { return { hintText: changeset.get("hintText"), }; diff --git a/packages/form-builder/addon/gql/fragments/field.graphql b/packages/form-builder/addon/gql/fragments/field.graphql index da98c5fa3d..74873478d9 100644 --- a/packages/form-builder/addon/gql/fragments/field.graphql +++ b/packages/form-builder/addon/gql/fragments/field.graphql @@ -99,7 +99,7 @@ fragment SimpleQuestion on Question { calcExpression hintText } - ... on FileQuestion { + ... on FilesQuestion { hintText } ... on ActionButtonQuestion { @@ -216,8 +216,8 @@ fragment SimpleAnswer on Answer { ... on ListAnswer { listValue: value } - ... on FileAnswer { - fileValue: value { + ... on FilesAnswer { + filesValue: value { id uploadUrl downloadUrl diff --git a/packages/form-builder/addon/gql/mutations/save-file-question.graphql b/packages/form-builder/addon/gql/mutations/save-files-question.graphql similarity index 56% rename from packages/form-builder/addon/gql/mutations/save-file-question.graphql rename to packages/form-builder/addon/gql/mutations/save-files-question.graphql index c5bea165c4..11c58caaa9 100644 --- a/packages/form-builder/addon/gql/mutations/save-file-question.graphql +++ b/packages/form-builder/addon/gql/mutations/save-files-question.graphql @@ -1,11 +1,11 @@ #import QuestionInfo from '../fragments/question-info.graphql' -mutation SaveFileQuestion($input: SaveFileQuestionInput!) { - saveFileQuestion(input: $input) { +mutation SaveFilesQuestion($input: SaveFilesQuestionInput!) { + saveFilesQuestion(input: $input) { question { id ...QuestionInfo - ... on FileQuestion { + ... on FilesQuestion { hintText } } diff --git a/packages/form-builder/addon/gql/queries/form-editor-question.graphql b/packages/form-builder/addon/gql/queries/form-editor-question.graphql index 079a9037ad..a2e6cd7a94 100644 --- a/packages/form-builder/addon/gql/queries/form-editor-question.graphql +++ b/packages/form-builder/addon/gql/queries/form-editor-question.graphql @@ -163,7 +163,7 @@ query FormEditorQuestion($slug: String!) { calcExpression hintText } - ... on FileQuestion { + ... on FilesQuestion { hintText } ... on ActionButtonQuestion { diff --git a/packages/form-builder/addon/validations/question.js b/packages/form-builder/addon/validations/question.js index ee8291d882..7a715a5b6a 100644 --- a/packages/form-builder/addon/validations/question.js +++ b/packages/form-builder/addon/validations/question.js @@ -19,7 +19,7 @@ export default { hintText: or( validateType("FormQuestion", true), validateType("StaticQuestion", true), - validateType("FileQuestion", true), + validateType("FilesQuestion", true), validateLength({ max: 1024, allowBlank: true }) ), integerMinValue: or( diff --git a/packages/form-builder/tests/integration/components/cfb-form-editor/question-test.js b/packages/form-builder/tests/integration/components/cfb-form-editor/question-test.js index 7c9d2fe20d..1bd97814b6 100644 --- a/packages/form-builder/tests/integration/components/cfb-form-editor/question-test.js +++ b/packages/form-builder/tests/integration/components/cfb-form-editor/question-test.js @@ -515,7 +515,7 @@ module("Integration | Component | cfb-form-editor/question", function (hooks) { this.server.create("form", { slug: "test-form" }); this.set("afterSubmit", (question) => { - assert.strictEqual(question.__typename, "FileQuestion"); + assert.strictEqual(question.__typename, "FilesQuestion"); assert.strictEqual(question.label, "Label"); assert.strictEqual(question.slug, "slug"); @@ -526,7 +526,7 @@ module("Integration | Component | cfb-form-editor/question", function (hooks) { hbs`` ); - await fillIn("[name=__typename]", "FileQuestion"); + await fillIn("[name=__typename]", "FilesQuestion"); await fillIn("[name=label]", "Label"); await fillIn("[name=slug]", "slug"); diff --git a/packages/form-builder/translations/de.yaml b/packages/form-builder/translations/de.yaml index b08c83409b..5bf27b1c08 100644 --- a/packages/form-builder/translations/de.yaml +++ b/packages/form-builder/translations/de.yaml @@ -103,7 +103,7 @@ caluma: TextareaQuestion: "Text (mehrzeilig)" TableQuestion: "Tabelle" FormQuestion: "Formular" - FileQuestion: "Datei" + FilesQuestion: "Dateien" StaticQuestion: "Nichtinteraktiver Inhalt" DateQuestion: "Datum" DynamicMultipleChoiceQuestion: "Dynamische Mehrfachauswahl" diff --git a/packages/form-builder/translations/en.yaml b/packages/form-builder/translations/en.yaml index 24c5dfca5f..d97e70b321 100644 --- a/packages/form-builder/translations/en.yaml +++ b/packages/form-builder/translations/en.yaml @@ -103,7 +103,7 @@ caluma: TextareaQuestion: "Textarea" TableQuestion: "Table" FormQuestion: "Form" - FileQuestion: "File" + FilesQuestion: "Files" StaticQuestion: "Non-interactive content" DateQuestion: "Date" DynamicMultipleChoiceQuestion: "Dynamic choices" diff --git a/packages/form-builder/translations/fr.yaml b/packages/form-builder/translations/fr.yaml index cce80d7c94..fc9d8affd3 100644 --- a/packages/form-builder/translations/fr.yaml +++ b/packages/form-builder/translations/fr.yaml @@ -102,7 +102,7 @@ caluma: TextareaQuestion: "Texte (plusieurs lignes)" TableQuestion: "Tableau" FormQuestion: "Formulaire" - FileQuestion: "Fichier" + FilesQuestion: "Fichiers" StaticQuestion: "Contenu non interactif" DateQuestion: "Date" DynamicMultipleChoiceQuestion: "Sélection multiple dynamique" diff --git a/packages/form/addon/components/cf-field-value.hbs b/packages/form/addon/components/cf-field-value.hbs index 3d9d8092be..c0499a3185 100644 --- a/packages/form/addon/components/cf-field-value.hbs +++ b/packages/form/addon/components/cf-field-value.hbs @@ -11,14 +11,14 @@ month="2-digit" year="numeric" }} -{{else if (has-question-type @field.question "file")}} - {{#if @field.answer.value}} +{{else if (has-question-type @field.question "files")}} + {{#each @field.answer.value as |file|}} - {{/if}} + {{/each}} {{else}} {{@field.answer.value}} {{/if}} \ No newline at end of file diff --git a/packages/form/addon/components/cf-field-value.js b/packages/form/addon/components/cf-field-value.js index 22ed6e34ac..59c0e03b2d 100644 --- a/packages/form/addon/components/cf-field-value.js +++ b/packages/form/addon/components/cf-field-value.js @@ -2,22 +2,23 @@ import { action } from "@ember/object"; import Component from "@glimmer/component"; import { queryManager } from "ember-apollo-client"; -import getFileAnswerInfoQuery from "@projectcaluma/ember-form/gql/queries/fileanswer-info.graphql"; +import getFilesAnswerInfoQuery from "@projectcaluma/ember-form/gql/queries/filesanswer-info.graphql"; export default class CfFieldValueComponent extends Component { @queryManager apollo; @action async download(id) { - const { downloadUrl } = await this.apollo.query( + const files = await this.apollo.query( { - query: getFileAnswerInfoQuery, - variables: { id }, + query: getFilesAnswerInfoQuery, + variables: { id: this.args.field.answer.raw.id }, fetchPolicy: "network-only", }, - "node.fileValue" + "node.value" ); + const { downloadUrl } = files?.find((file) => file.id === id); if (downloadUrl) { window.open(downloadUrl, "_blank"); } diff --git a/packages/form/addon/components/cf-field/input.js b/packages/form/addon/components/cf-field/input.js index 86016942dd..16496fa0d6 100644 --- a/packages/form/addon/components/cf-field/input.js +++ b/packages/form/addon/components/cf-field/input.js @@ -4,7 +4,7 @@ import Component from "@glimmer/component"; import ActionButtonComponent from "@projectcaluma/ember-form/components/cf-field/input/action-button"; import CheckboxComponent from "@projectcaluma/ember-form/components/cf-field/input/checkbox"; import DateComponent from "@projectcaluma/ember-form/components/cf-field/input/date"; -import FileComponent from "@projectcaluma/ember-form/components/cf-field/input/file"; +import FilesComponent from "@projectcaluma/ember-form/components/cf-field/input/files"; import FloatComponent from "@projectcaluma/ember-form/components/cf-field/input/float"; import IntegerComponent from "@projectcaluma/ember-form/components/cf-field/input/integer"; import RadioComponent from "@projectcaluma/ember-form/components/cf-field/input/radio"; @@ -20,7 +20,7 @@ const COMPONENT_MAPPING = { DateQuestion: DateComponent, DynamicChoiceQuestion: RadioComponent, DynamicMultipleChoiceQuestion: CheckboxComponent, - FileQuestion: FileComponent, + FilesQuestion: FilesComponent, FloatQuestion: FloatComponent, IntegerQuestion: IntegerComponent, MultipleChoiceQuestion: CheckboxComponent, diff --git a/packages/form/addon/components/cf-field/input/file.hbs b/packages/form/addon/components/cf-field/input/file.hbs deleted file mode 100644 index 32c90a5c57..0000000000 --- a/packages/form/addon/components/cf-field/input/file.hbs +++ /dev/null @@ -1,32 +0,0 @@ -
-
- - - - {{t "caluma.form.selectFile"}} - -
- {{#if (and this.downloadUrl this.downloadName)}} -
- - {{this.downloadName}} - - -
- {{/if}} -
\ No newline at end of file diff --git a/packages/form/addon/components/cf-field/input/file.js b/packages/form/addon/components/cf-field/input/file.js deleted file mode 100644 index 9c97a9b113..0000000000 --- a/packages/form/addon/components/cf-field/input/file.js +++ /dev/null @@ -1,89 +0,0 @@ -import { action } from "@ember/object"; -import { inject as service } from "@ember/service"; -import Component from "@glimmer/component"; -import { queryManager } from "ember-apollo-client"; -import fetch from "fetch"; - -import removeAnswerMutation from "@projectcaluma/ember-form/gql/mutations/remove-answer.graphql"; -import getFileAnswerInfoQuery from "@projectcaluma/ember-form/gql/queries/fileanswer-info.graphql"; - -export default class CfFieldInputFileComponent extends Component { - @service intl; - - @queryManager apollo; - - get downloadUrl() { - return this.args.field?.answer?.value?.downloadUrl; - } - - get downloadName() { - return this.args.field?.answer?.value?.name; - } - - @action - async download() { - const { downloadUrl } = await this.apollo.query( - { - query: getFileAnswerInfoQuery, - variables: { id: this.args.field.answer.raw.id }, - fetchPolicy: "network-only", - }, - "node.fileValue" - ); - - if (downloadUrl) { - window.open(downloadUrl, "_blank"); - } - } - - @action - async save({ target }) { - const file = target.files[0]; - - if (!file) { - return; - } - - const { fileValue } = await this.args.onSave(file.name); - - try { - const response = await fetch(fileValue.uploadUrl, { - method: "PUT", - body: file, - }); - - if (!response.ok) { - throw new Error(); - } - - this.args.field.answer.value = { - name: file.name, - downloadUrl: fileValue.downloadUrl, - }; - } catch (error) { - await this.args.onSave(null); - this.args.field._errors = [{ type: "uploadFailed" }]; - } finally { - // eslint-disable-next-line require-atomic-updates - target.value = ""; - } - } - - @action - async delete() { - try { - await this.apollo.mutate({ - mutation: removeAnswerMutation, - variables: { - input: { - answer: this.args.field.answer.uuid, - }, - }, - }); - - await this.args.onSave(null); - } catch (error) { - this.args.field._errors = [{ type: "deleteFailed" }]; - } - } -} diff --git a/packages/form/addon/components/cf-field/input/files.hbs b/packages/form/addon/components/cf-field/input/files.hbs new file mode 100644 index 0000000000..95462c227e --- /dev/null +++ b/packages/form/addon/components/cf-field/input/files.hbs @@ -0,0 +1,35 @@ +
+
+ + + + {{t "caluma.form.selectFile"}} + +
+
    + {{#each this.files as |file|}} +
  • + + {{file.name}} + + +
  • + {{/each}} +
+
\ No newline at end of file diff --git a/packages/form/addon/components/cf-field/input/files.js b/packages/form/addon/components/cf-field/input/files.js new file mode 100644 index 0000000000..11170ed678 --- /dev/null +++ b/packages/form/addon/components/cf-field/input/files.js @@ -0,0 +1,113 @@ +import { action } from "@ember/object"; +import { inject as service } from "@ember/service"; +import { macroCondition, isTesting } from "@embroider/macros"; +import Component from "@glimmer/component"; +import { queryManager } from "ember-apollo-client"; +import fetch from "fetch"; + +import getFilesAnswerInfoQuery from "@projectcaluma/ember-form/gql/queries/filesanswer-info.graphql"; + +export default class CfFieldInputFilesComponent extends Component { + @service intl; + + @queryManager apollo; + + get files() { + return this.args.field?.answer?.value; + } + + @action + async download(fileId) { + if (!fileId) { + return; + } + const answers = await this.apollo.query( + { + query: getFilesAnswerInfoQuery, + variables: { id: this.args.field.answer.raw.id }, + fetchPolicy: "network-only", + }, + "node.value" + ); + const { downloadUrl } = + answers.find((file) => + // the testing graph-ql setup does a base64 encoding of `__typename: fileID` + macroCondition(isTesting()) + ? file.id === fileId || + atob(file.id).substring(file.__typename.length + 1) === fileId + : file.id === fileId + ) ?? {}; + if (downloadUrl) { + window.open(downloadUrl, "_blank"); + } + } + + @action + async save({ target }) { + // store the old list of files + // unwrap files from FileList construct + let newFiles = Array.from(target.files).map((file) => ({ + name: file.name, + value: file, + })); + + const fileList = [...(this.files || []), ...newFiles]; + + if (newFiles.length === 0) { + return; + } + + // trigger save action for file list of old and new files with + // reduces properties to match gql format + const { filesValue: savedAnswerValue } = await this.args.onSave( + fileList.map(({ name, id }) => ({ name, id })) + ); + + try { + // iterate over list of new files and enrich with graphql answer values + newFiles = newFiles.map((file) => ({ + ...savedAnswerValue.find( + (value) => + file.name === value.name && + !fileList.find((file) => file.id === value.id) + ), + value: file.value, + })); + + const uploadFunction = async (file) => { + const response = await fetch(file.uploadUrl, { + method: "PUT", + body: file.value, + }); + if (!response.ok) { + throw new Error(); + } + return response; + }; + + // upload the actual file to data storage + await Promise.all(newFiles.map((file) => uploadFunction(file))); + + this.args.field.answer.value = savedAnswerValue; + } catch (error) { + await this.args.onSave([]); + this.args.field._errors = [{ type: "uploadFailed" }]; + } finally { + // eslint-disable-next-line require-atomic-updates + target.value = ""; + } + } + + @action + async delete(fileId) { + const remainingFiles = this.files + .filter((file) => file.id !== fileId) + .map(({ name, id }) => ({ name, id })); + + try { + await this.args.onSave(remainingFiles); + } catch (error) { + this.args.field._errors = [{ type: "deleteFailed" }]; + } + } +} diff --git a/packages/form/addon/gql/fragments/field.graphql b/packages/form/addon/gql/fragments/field.graphql index d61fcf8a4f..18a1e0e0f6 100644 --- a/packages/form/addon/gql/fragments/field.graphql +++ b/packages/form/addon/gql/fragments/field.graphql @@ -117,7 +117,7 @@ fragment SimpleQuestion on Question { calcExpression hintText } - ... on FileQuestion { + ... on FilesQuestion { hintText } ... on ActionButtonQuestion { @@ -234,8 +234,8 @@ fragment SimpleAnswer on Answer { ... on ListAnswer { listValue: value } - ... on FileAnswer { - fileValue: value { + ... on FilesAnswer { + filesValue: value { id uploadUrl downloadUrl diff --git a/packages/form/addon/gql/mutations/remove-answer.graphql b/packages/form/addon/gql/mutations/remove-answer.graphql deleted file mode 100644 index 03fcdd131d..0000000000 --- a/packages/form/addon/gql/mutations/remove-answer.graphql +++ /dev/null @@ -1,7 +0,0 @@ -mutation RemoveAnswer($input: RemoveAnswerInput!) { - removeAnswer(input: $input) { - answer { - id - } - } -} diff --git a/packages/form/addon/gql/mutations/save-document-file-answer.graphql b/packages/form/addon/gql/mutations/save-document-file-answer.graphql deleted file mode 100644 index bcec5752d5..0000000000 --- a/packages/form/addon/gql/mutations/save-document-file-answer.graphql +++ /dev/null @@ -1,9 +0,0 @@ -#import * from '../fragments/field.graphql' - -mutation SaveDocumentFileAnswer($input: SaveDocumentFileAnswerInput!) { - saveDocumentFileAnswer(input: $input) { - answer { - ...FieldAnswer - } - } -} diff --git a/packages/form/addon/gql/mutations/save-document-files-answer.graphql b/packages/form/addon/gql/mutations/save-document-files-answer.graphql new file mode 100644 index 0000000000..764b1c1c19 --- /dev/null +++ b/packages/form/addon/gql/mutations/save-document-files-answer.graphql @@ -0,0 +1,9 @@ +#import * from '../fragments/field.graphql' + +mutation SaveDocumentFilesAnswer($input: SaveDocumentFilesAnswerInput!) { + saveDocumentFilesAnswer(input: $input) { + answer { + ...FieldAnswer + } + } +} diff --git a/packages/form/addon/gql/queries/fileanswer-info.graphql b/packages/form/addon/gql/queries/filesanswer-info.graphql similarity index 56% rename from packages/form/addon/gql/queries/fileanswer-info.graphql rename to packages/form/addon/gql/queries/filesanswer-info.graphql index e566f73898..f56efbf0aa 100644 --- a/packages/form/addon/gql/queries/fileanswer-info.graphql +++ b/packages/form/addon/gql/queries/filesanswer-info.graphql @@ -1,8 +1,8 @@ -query FileAnswerInfo($id: ID!) { +query FilesAnswerInfo($id: ID!) { node(id: $id) { - id - ... on FileAnswer { - fileValue: value { + ... on FilesAnswer { + id + value { id uploadUrl downloadUrl diff --git a/packages/form/addon/lib/field.js b/packages/form/addon/lib/field.js index 28f44a38d2..d0dfe92643 100644 --- a/packages/form/addon/lib/field.js +++ b/packages/form/addon/lib/field.js @@ -13,7 +13,7 @@ import { cached } from "tracked-toolbox"; import { decodeId } from "@projectcaluma/ember-core/helpers/decode-id"; import saveDocumentDateAnswerMutation from "@projectcaluma/ember-form/gql/mutations/save-document-date-answer.graphql"; -import saveDocumentFileAnswerMutation from "@projectcaluma/ember-form/gql/mutations/save-document-file-answer.graphql"; +import saveDocumentFilesAnswerMutation from "@projectcaluma/ember-form/gql/mutations/save-document-files-answer.graphql"; import saveDocumentFloatAnswerMutation from "@projectcaluma/ember-form/gql/mutations/save-document-float-answer.graphql"; import saveDocumentIntegerAnswerMutation from "@projectcaluma/ember-form/gql/mutations/save-document-integer-answer.graphql"; import saveDocumentListAnswerMutation from "@projectcaluma/ember-form/gql/mutations/save-document-list-answer.graphql"; @@ -34,7 +34,7 @@ export const TYPE_MAP = { DynamicChoiceQuestion: "StringAnswer", TableQuestion: "TableAnswer", FormQuestion: null, - FileQuestion: "FileAnswer", + FilesQuestion: "FilesAnswer", StaticQuestion: null, DateQuestion: "DateAnswer", }; @@ -44,7 +44,7 @@ const MUTATION_MAP = { IntegerAnswer: saveDocumentIntegerAnswerMutation, StringAnswer: saveDocumentStringAnswerMutation, ListAnswer: saveDocumentListAnswerMutation, - FileAnswer: saveDocumentFileAnswerMutation, + FilesAnswer: saveDocumentFilesAnswerMutation, DateAnswer: saveDocumentDateAnswerMutation, TableAnswer: saveDocumentTableAnswerMutation, }; @@ -810,11 +810,11 @@ export default class Field extends Base { /** * Dummy method for the validation of file uploads. * - * @method _validateFileQuestion + * @method _validateFilesQuestion * @return {Boolean} Always returns true * @private */ - _validateFileQuestion() { + _validateFilesQuestion() { return true; } diff --git a/packages/form/app/components/cf-field/input/file.js b/packages/form/app/components/cf-field/input/files.js similarity index 75% rename from packages/form/app/components/cf-field/input/file.js rename to packages/form/app/components/cf-field/input/files.js index d2e2d37ab8..28a75c2f31 100644 --- a/packages/form/app/components/cf-field/input/file.js +++ b/packages/form/app/components/cf-field/input/files.js @@ -1 +1 @@ -export { default } from "@projectcaluma/ember-form/components/cf-field/input/file"; +export { default } from "@projectcaluma/ember-form/components/cf-field/input/files"; diff --git a/packages/form/tests/integration/components/cf-content-test.js b/packages/form/tests/integration/components/cf-content-test.js index c0517b8ce6..5a15e8c9d9 100644 --- a/packages/form/tests/integration/components/cf-content-test.js +++ b/packages/form/tests/integration/components/cf-content-test.js @@ -1,4 +1,4 @@ -import { render, fillIn, click } from "@ember/test-helpers"; +import { render, fillIn, click, triggerEvent } from "@ember/test-helpers"; import { hbs } from "ember-cli-htmlbars"; import { setupMirage } from "ember-cli-mirage/test-support"; import { setupIntl } from "ember-intl/test-support"; @@ -44,6 +44,10 @@ module("Integration | Component | cf-content", function (hooks) { formIds: [form.id], type: "DATE", }), + this.server.create("question", { + formIds: [form.id], + type: "FILES", + }), ]; const document = this.server.create("document", { formId: form.id }); @@ -88,6 +92,10 @@ module("Integration | Component | cf-content", function (hooks) { year: "numeric", }) ); + } else if (answer.type === "FILES") { + assert + .dom(`[data-test-file-list]`) + .containsText(answer.value?.[0]?.name); } else { assert.dom(`[name="${id}"]`).hasValue(String(answer.value)); } @@ -115,6 +123,8 @@ module("Integration | Component | cf-content", function (hooks) { .forEach(({ slug }) => { assert.dom(`[name="${id}"][value="${slug}"]`).isDisabled(); }); + } else if (question.type === "FILES") { + assert.dom(`[name="${id}"]`).isDisabled(); } else { assert.dom(`[name="${id}"]`).hasAttribute("readonly"); assert.dom(`[name="${id}"]`).hasClass("uk-disabled"); @@ -166,15 +176,11 @@ module("Integration | Component | cf-content", function (hooks) { slug: "date-question", type: "DATE", }); - // The following questions is commented-out as we currently have a - // problem with GraphQL/Mirage and I didn't want to skip everything. - /* this.server.create("question", { formIds: [form.id], - slug: "file-question", - type: "FILE" + slug: "files-question", + type: "FILES", }); - */ radioQuestion.options.models.forEach((option, i) => { option.update({ slug: `${radioQuestion.slug}-option-${i + 1}` }); @@ -217,15 +223,12 @@ module("Integration | Component | cf-content", function (hooks) { ); await click(`[name="Document:${document.id}:Question:date-question"]`); await Pikaday.selectDate(new Date(2019, 2, 25)); // month is zero based - // The following answers are commented-out as we currently have a - // problem with GraphQL/Mirage and I didn't want to skip everything. - /* + await triggerEvent( - `[name="Document:${document.id}:Question:file-question"]`, + `[name="Document:${document.id}:Question:files-question"]`, "change", - [new File(["test"], "test.txt")] + { files: [new File(["test"], "test.txt")] } ); - */ assert.deepEqual( this.server.schema.documents @@ -263,14 +266,14 @@ module("Integration | Component | cf-content", function (hooks) { slug: "date-question", value: "2019-03-25", }, - // The following answers are commented-out as we currently have a - // problem with GraphQL/Mirage and I didn't want to skip everything. - /*, { - slug: "file-question", - value: { metadata: { object_name: "test.txt" } } - } - */ + slug: "files-question", + value: [], + // This acutally should be the value underneath, but apollo replaces our uploadUrl + // to "Hello World" and ruins the show this way. Afterwards a catch block will reset the + // value to an empty array. + // value: [{ name: "test.txt" }] + }, ] ); }); diff --git a/packages/form/tests/integration/components/cf-field-value-test.js b/packages/form/tests/integration/components/cf-field-value-test.js index 34a405edf7..bf5b2c0e99 100644 --- a/packages/form/tests/integration/components/cf-field-value-test.js +++ b/packages/form/tests/integration/components/cf-field-value-test.js @@ -1,11 +1,13 @@ import { render } from "@ember/test-helpers"; import { hbs } from "ember-cli-htmlbars"; +import { setupMirage } from "ember-cli-mirage/test-support"; import { setupIntl } from "ember-intl/test-support"; import { setupRenderingTest } from "ember-qunit"; import { module, test } from "qunit"; module("Integration | Component | cf-field-value", function (hooks) { setupRenderingTest(hooks); + setupMirage(hooks); setupIntl(hooks); test("it renders multiple choice questions", async function (assert) { @@ -110,4 +112,24 @@ module("Integration | Component | cf-field-value", function (hooks) { assert.dom(this.element).hasText("foo"); }); + + test("it renders file questions", async function (assert) { + const file = this.server.create("file"); + + this.field = { + questionType: "FilesQuestion", + question: { + raw: { + __typename: "FilesQuestion", + }, + }, + answer: { + value: [file], + }, + }; + + await render(hbs``); + + assert.dom(this.element).hasText(file.name); + }); }); diff --git a/packages/form/tests/integration/components/cf-field/input/file-test.js b/packages/form/tests/integration/components/cf-field/input/file-test.js deleted file mode 100644 index 8948b2900f..0000000000 --- a/packages/form/tests/integration/components/cf-field/input/file-test.js +++ /dev/null @@ -1,97 +0,0 @@ -import { render, triggerEvent, click } from "@ember/test-helpers"; -import { tracked } from "@glimmer/tracking"; -import { hbs } from "ember-cli-htmlbars"; -import { setupMirage } from "ember-cli-mirage/test-support"; -import { setupIntl } from "ember-intl/test-support"; -import { setupRenderingTest } from "ember-qunit"; -import { module, test } from "qunit"; - -module("Integration | Component | cf-field/input/file", function (hooks) { - setupRenderingTest(hooks); - setupMirage(hooks); - setupIntl(hooks); - - test("it computes the proper element id", async function (assert) { - await render(hbs`{{cf-field/input/file field=(hash pk="test-id")}}`); - - assert.dom("#test-id").exists(); - }); - - test("it allows to upload a file", async function (assert) { - assert.expect(6); - - this.field = new (class { - answer = { - raw: { - id: btoa("FileAnswer:1"), - }, - value: {}, - }; - @tracked _errors = []; - })(); - - this.onSave = (name) => ({ - fileValue: { uploadUrl: `/minio/upload/${name}` }, - }); - - const payload_good = new File(["test"], "good.txt", { type: "text/plain" }); - const payload_fail = new File(["test"], "fail.txt", { type: "text/plain" }); - - await render( - hbs`` - ); - - await triggerEvent("input[type=file]", "change", { files: [] }); - assert.strictEqual(this.field.answer.value.name, undefined); - assert.deepEqual(this.field._errors, []); - - await triggerEvent("input[type=file]", "change", { files: [payload_fail] }); - assert.strictEqual(this.field.answer.value.name, undefined); - assert.deepEqual(this.field._errors, [{ type: "uploadFailed" }]); - - // reset errors - this.field._errors = []; - - await triggerEvent("input[type=file]", "change", { files: [payload_good] }); - assert.strictEqual(this.field.answer.value.name, "good.txt"); - assert.deepEqual(this.field._errors, []); - }); - - test("it allows to download a file", async function (assert) { - assert.expect(4); - - this.server.create("file"); - - this.field = { - answer: { - raw: { - id: btoa("FileAnswer:1"), - }, - value: { - downloadUrl: "/minio/download/good.txt", - name: "good.txt", - }, - }, - }; - - // Hijack window.open - const window_open = window.open; - window.open = (url, target) => { - assert.ok(url.startsWith("http"), "The URL is a HTTP address"); - assert.strictEqual(target, "_blank", "Target for new window is _blank"); - }; - - await render(hbs``); - - assert.dom("[data-test-download-link]").exists(); - assert - .dom("[data-test-download-link]") - .hasText(this.field.answer.value.name); - - await click("[data-test-download-link]"); - - // Restore window.open - // eslint-disable-next-line require-atomic-updates - window.open = window_open; - }); -}); diff --git a/packages/form/tests/integration/components/cf-field/input/files-test.js b/packages/form/tests/integration/components/cf-field/input/files-test.js new file mode 100644 index 0000000000..1b7eb1c6b0 --- /dev/null +++ b/packages/form/tests/integration/components/cf-field/input/files-test.js @@ -0,0 +1,113 @@ +import { render, triggerEvent, click } from "@ember/test-helpers"; +import { faker } from "@faker-js/faker"; +import { tracked } from "@glimmer/tracking"; +import { hbs } from "ember-cli-htmlbars"; +import { setupMirage } from "ember-cli-mirage/test-support"; +import { setupIntl } from "ember-intl/test-support"; +import { setupRenderingTest } from "ember-qunit"; +import { module, test } from "qunit"; + +module("Integration | Component | cf-field/input/files", function (hooks) { + setupRenderingTest(hooks); + setupMirage(hooks); + setupIntl(hooks, ["en"]); + + test("it computes the proper element id", async function (assert) { + await render(hbs``); + + assert.dom("#test-id").exists(); + }); + + test("it allows to upload files", async function (assert) { + assert.expect(10); + + this.field = new (class { + answer = { + raw: { + id: btoa("FilesAnswer:1"), + }, + value: null, + }; + @tracked _errors = []; + })(); + + this.onSave = (files) => ({ + filesValue: files?.map((f) => ({ + name: f.name, + id: faker.datatype.uuid(), + uploadUrl: `/minio/upload/${f.name}`, + })), + }); + + const payload_good_1 = new File(["test"], "good-1.txt", { + type: "text/plain", + }); + const payload_good_2 = new File(["test"], "good-2.txt", { + type: "text/plain", + }); + const payload_fail = new File(["test"], "fail.txt", { type: "text/plain" }); + + await render( + hbs`` + ); + + await triggerEvent("input[type=file]", "change", { files: [] }); + assert.strictEqual(this.field.answer.value, undefined); + assert.deepEqual(this.field._errors, []); + + await triggerEvent("input[type=file]", "change", { files: [payload_fail] }); + assert.strictEqual(this.field.answer.value, undefined); + assert.deepEqual(this.field._errors, [{ type: "uploadFailed" }]); + + // reset errors + this.field._errors = []; + + await triggerEvent("input[type=file]", "change", { + files: [payload_good_1], + }); + assert.strictEqual(this.field.answer.value?.[0]?.name, "good-1.txt"); + assert.deepEqual(this.field._errors, []); + + await triggerEvent("input[type=file]", "change", { + files: [payload_good_1, payload_good_2], + }); + + assert.strictEqual(this.field.answer.value?.[0]?.name, "good-1.txt"); + assert.strictEqual(this.field.answer.value?.[1]?.name, "good-1.txt"); + assert.strictEqual(this.field.answer.value?.[2]?.name, "good-2.txt"); + assert.deepEqual(this.field._errors, []); + }); + + test("it allows to download a file", async function (assert) { + assert.expect(4); + + const file = this.server.create("file"); + + this.field = { + answer: { + raw: { + id: btoa("FilesAnswer:1"), + }, + value: [file], + }, + }; + + // Hijack window.open + const window_open = window.open; + window.open = (url, target) => { + assert.ok(url.startsWith("http"), "The URL is a HTTP address"); + assert.strictEqual(target, "_blank", "Target for new window is _blank"); + }; + + await render(hbs``); + + assert.dom(`[data-test-download-link="${file.id}"]`).exists(); + assert.dom(`[data-test-download-link="${file.id}"]`).hasText(file.name); + + await click("[data-test-download-link]"); + + // Restore window.open + // eslint-disable-next-line require-atomic-updates + window.open = window_open; + }); +}); diff --git a/packages/testing/addon-mirage-support/factories/answer.js b/packages/testing/addon-mirage-support/factories/answer.js index 34c0af5e7d..6c717c8c77 100644 --- a/packages/testing/addon-mirage-support/factories/answer.js +++ b/packages/testing/addon-mirage-support/factories/answer.js @@ -58,15 +58,19 @@ export default Factory.extend({ .slug, }); } - } else if (answer.question.type === "FILE") { - answer.update({ type: "FILE" }); + } else if (answer.question.type === "FILES") { + answer.update({ type: "FILES" }); if (answer.value === undefined) { answer.update({ - value: { - uploadUrl: faker.internet.url, - downloadUrl: faker.internet.url, - }, + value: [ + { + id: faker.datatype.uuid(), + name: faker.datatype.string(), + uploadUrl: faker.internet.url(), + downloadUrl: faker.internet.url(), + }, + ], }); } } else if (answer.question.type === "DATE") { diff --git a/packages/testing/addon-mirage-support/factories/file.js b/packages/testing/addon-mirage-support/factories/file.js index 7606002343..8d7adca8bf 100644 --- a/packages/testing/addon-mirage-support/factories/file.js +++ b/packages/testing/addon-mirage-support/factories/file.js @@ -3,7 +3,7 @@ import { Factory } from "miragejs"; export default Factory.extend({ id: () => faker.datatype.uuid(), - createdAt: () => faker.date.past(), + name: () => faker.datatype.string(), modifiedAt: () => faker.date.past(), createdByUser: () => faker.datatype.uuid(), uploadUrl: () => faker.internet.url(), diff --git a/packages/testing/addon/mirage-graphql/mocks/answer.js b/packages/testing/addon/mirage-graphql/mocks/answer.js index 2062406dc1..a72059b43d 100644 --- a/packages/testing/addon/mirage-graphql/mocks/answer.js +++ b/packages/testing/addon/mirage-graphql/mocks/answer.js @@ -61,12 +61,12 @@ export default class AnswerMock extends BaseMock { }); } - @register("SaveDocumentFileAnswerPayload") - handleSaveFileAnswer(_, { input }) { + @register("SaveDocumentFilesAnswerPayload") + handleSaveFilesAnswer(_, { input }) { return this._handleSaveDocumentAnswer(_, { ...input, - value: input.value ? { metadata: { object_name: input.value } } : null, - type: "FILE", + value: input.value ? [...input.value] : [], + type: "FILES", }); } diff --git a/packages/testing/addon/mirage-graphql/mocks/base.js b/packages/testing/addon/mirage-graphql/mocks/base.js index 663f0a51c7..b8c02c5a29 100644 --- a/packages/testing/addon/mirage-graphql/mocks/base.js +++ b/packages/testing/addon/mirage-graphql/mocks/base.js @@ -10,7 +10,7 @@ import serialize from "@projectcaluma/ember-testing/mirage-graphql/serialize"; export const ANSWER_TYPES = [ "DATE", - "FILE", + "FILES", "FLOAT", "INTEGER", "LIST", @@ -25,7 +25,7 @@ export const QUESTION_TYPES = [ "DATE", "DYNAMIC_CHOICE", "DYNAMIC_MULTIPLE_CHOICE", - "FILE", + "FILES", "FLOAT", "FORM", "INTEGER", diff --git a/packages/testing/addon/mirage-graphql/mocks/question.js b/packages/testing/addon/mirage-graphql/mocks/question.js index b35f6a21bf..4887e07a28 100644 --- a/packages/testing/addon/mirage-graphql/mocks/question.js +++ b/packages/testing/addon/mirage-graphql/mocks/question.js @@ -92,10 +92,10 @@ export default class QuestionMock extends BaseMock { }); } - @register("SaveFileQuestionPayload") - handleSaveFileQuestion(_, { input }) { + @register("SaveFilesQuestionPayload") + handleSaveFilesQuestion(_, { input }) { return this.handleSavePayload.fn.call(this, _, { - input: { ...input, type: "FILE" }, + input: { ...input, type: "FILES" }, }); } } diff --git a/packages/testing/addon/mirage-graphql/schema.graphql b/packages/testing/addon/mirage-graphql/schema.graphql index e40bb3ece6..0278c6f675 100644 --- a/packages/testing/addon/mirage-graphql/schema.graphql +++ b/packages/testing/addon/mirage-graphql/schema.graphql @@ -1388,13 +1388,13 @@ type File implements Node { """ id: ID! name: String! - answer: FileAnswer + answer: FilesAnswer uploadUrl: String downloadUrl: String metadata: GenericScalar } -type FileAnswer implements Answer & Node { +type FilesAnswer implements Answer & Node { createdAt: DateTime! modifiedAt: DateTime! createdByUser: String @@ -1407,12 +1407,12 @@ type FileAnswer implements Answer & Node { """ id: ID! question: Question! - value: File! + value: [File]! meta: GenericScalar! file: File } -type FileQuestion implements Question & Node { +type FilesQuestion implements Question & Node { createdAt: DateTime! modifiedAt: DateTime! createdByUser: String @@ -2000,7 +2000,7 @@ type Mutation { ): SaveIntegerQuestionPayload saveTableQuestion(input: SaveTableQuestionInput!): SaveTableQuestionPayload saveFormQuestion(input: SaveFormQuestionInput!): SaveFormQuestionPayload - saveFileQuestion(input: SaveFileQuestionInput!): SaveFileQuestionPayload + saveFilesQuestion(input: SaveFilesQuestionInput!): SaveFilesQuestionPayload saveStaticQuestion(input: SaveStaticQuestionInput!): SaveStaticQuestionPayload saveCalculatedFloatQuestion( input: SaveCalculatedFloatQuestionInput! @@ -2028,9 +2028,9 @@ type Mutation { saveDocumentTableAnswer( input: SaveDocumentTableAnswerInput! ): SaveDocumentTableAnswerPayload - saveDocumentFileAnswer( - input: SaveDocumentFileAnswerInput! - ): SaveDocumentFileAnswerPayload + saveDocumentFilesAnswer( + input: SaveDocumentFilesAnswerInput! + ): SaveDocumentFilesAnswerPayload saveDefaultStringAnswer( input: SaveDefaultStringAnswerInput! ): SaveDefaultStringAnswerPayload @@ -2840,15 +2840,20 @@ type SaveDocumentDateAnswerPayload { clientMutationId: String } -input SaveDocumentFileAnswerInput { +input SaveDocumentFilesAnswerInput { question: ID! document: ID! meta: JSONString - value: String + value: [SaveFile] clientMutationId: String } -type SaveDocumentFileAnswerPayload { +input SaveFile { + id: String + name: String +} + +type SaveDocumentFilesAnswerPayload { answer: Answer clientMutationId: String } @@ -2970,7 +2975,7 @@ type SaveDynamicMultipleChoiceQuestionPayload { clientMutationId: String } -input SaveFileQuestionInput { +input SaveFilesQuestionInput { slug: String! label: String! infoText: String @@ -2982,7 +2987,7 @@ input SaveFileQuestionInput { clientMutationId: String } -type SaveFileQuestionPayload { +type SaveFilesQuestionPayload { question: Question clientMutationId: String }