Skip to content

Commit

Permalink
refactor(files): accept multiple files
Browse files Browse the repository at this point in the history
  • Loading branch information
derrabauke committed Jul 5, 2022
1 parent 462c744 commit aed8ca1
Show file tree
Hide file tree
Showing 10 changed files with 91 additions and 72 deletions.
2 changes: 1 addition & 1 deletion packages/form-builder/addon/gql/fragments/field.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ fragment SimpleAnswer on Answer {
listValue: value
}
... on FilesAnswer {
fileValue: value {
filesValue: value {
id
uploadUrl
downloadUrl
Expand Down
8 changes: 4 additions & 4 deletions packages/form/addon/components/cf-field-value.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@
year="numeric"
}}
{{else if (has-question-type @field.question "file")}}
{{#if @field.answer.value}}
{{#each @field.answer.value as |file|}}
<UkButton
@color="link"
@label={{@field.answer.value.name}}
@onClick={{fn this.download @field.answer.raw.id}}
@label={{file.name}}
@onClick={{fn this.download file.id}}
/>
{{/if}}
{{/each}}
{{else}}
{{@field.answer.value}}
{{/if}}
2 changes: 1 addition & 1 deletion packages/form/addon/components/cf-field-value.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export default class CfFieldValueComponent extends Component {
const files = await this.apollo.query(
{
query: getFilesAnswerInfoQuery,
variables: { id },
variables: { id: this.args.field.answer.raw.id },
fetchPolicy: "network-only",
},
"node.value"
Expand Down
38 changes: 21 additions & 17 deletions packages/form/addon/components/cf-field/input/file.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,31 @@
name={{@field.pk}}
id={{@field.pk}}
disabled={{@disabled}}
accept={{this.allowedMimeTypes}}
multiple
{{on "change" this.save}}
/>
<UkButton disabled={{@disabled}}>
{{t "caluma.form.selectFile"}}
</UkButton>
</div>
{{#if (and this.downloadUrl this.downloadName)}}
<div>
<UkButton
data-test-download-link
@color="link"
@onClick={{this.download}}
>
{{this.downloadName}}
</UkButton>
<UkIcon
class="uk-icon-button uk-margin-small-left"
role="button"
@icon="trash"
{{on "click" this.delete}}
/>
</div>
{{/if}}
<ul class="uk-list uk-list-collapse">
{{#each this.files as |file|}}
<li class="uk-text-justify uk-text-middle">
<UkButton
data-test-download-link
@color="link"
@onClick={{fn this.download file.id}}
>
{{file.name}}
</UkButton>
<UkIcon
class="uk-icon-button uk-margin-small-left"
role="button"
@icon="trash"
{{on "click" (fn this.delete file.id)}}
/>
</li>
{{/each}}
</ul>
</div>
93 changes: 55 additions & 38 deletions packages/form/addon/components/cf-field/input/file.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,64 +4,86 @@ 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 getFilesAnswerInfoQuery from "@projectcaluma/ember-form/gql/queries/filesanswer-info.graphql";

export default class CfFieldInputFileComponent extends Component {
@service intl;

@queryManager apollo;

// TODO: refactor for multiple files

get downloadUrl() {
return this.args.field?.answer?.value?.downloadUrl;
get files() {
return this.args.field?.answer?.value;
}

get downloadName() {
return this.args.field?.answer?.value?.name;
get allowedMimeTypes() {
// TODO: put this into a config env value
return ["image/png", "image/jpeg", "application/pdf"];
}

@action
async download() {
const { downloadUrl } = await this.apollo.query(
async download(fileId) {
// TODO: We have the downloadUrl already. Do we need to refetch?
const answers = await this.apollo.query(
{
query: getFilesAnswerInfoQuery,
variables: { id: this.args.field.answer.raw.id },
fetchPolicy: "network-only",
},
"node"
"node.value"
);

const { downloadUrl } = answers.find((file) => file.id === fileId);
if (downloadUrl) {
window.open(downloadUrl, "_blank");
}
}

@action
async save({ target }) {
const file = target.files[0];

if (!file) {
// store the old list of files
const fileList = this.files.slice();

// unwrap files from FileList construct
const newFiles = [];
for (let i = 0; i < target.files.length; i++) {
newFiles.push({ name: target.files[i].name });
if (
!fileList.find((existing) => existing.name === target.files[i].name)
) {
fileList.push({ name: target.files[i].name });
}
}
if (!newFiles) {
return;
}

const { fileValue } = await this.args.onSave(file.name);
// trigger save action for deduped file list of old and new files reduces properties
const { filesValue } = await this.args.onSave(
fileList.map(({ name, id }) => ({ name, id }))
);

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,
};
// iterate over list of new files and map to graphql answer values
// this way only upload new files selected by the input
newFiles
.map((file) => filesValue.find((value) => file.name === value.name))
.forEach(async (file) => {
const response = await fetch(file.uploadUrl, {
method: "PUT",
body: file,
});

if (!response.ok) {
throw new Error();
}
// replace fileList value with filesValue value
fileList.splice(
fileList.indexOf((f) => f.name === file.name),
1,
file
);
});
this.args.field.answer.value = fileList;
} catch (error) {
await this.args.onSave(null);
this.args.field._errors = [{ type: "uploadFailed" }];
Expand All @@ -72,18 +94,13 @@ export default class CfFieldInputFileComponent extends Component {
}

@action
async delete() {
try {
await this.apollo.mutate({
mutation: removeAnswerMutation,
variables: {
input: {
answer: this.args.field.answer.uuid,
},
},
});
async delete(fileId) {
const remainingFiles = this.files
.filter((file) => file.id !== fileId)
.map(({ name, id }) => ({ name, id }));

await this.args.onSave(null);
try {
await this.args.onSave(remainingFiles);
} catch (error) {
this.args.field._errors = [{ type: "deleteFailed" }];
}
Expand Down
2 changes: 1 addition & 1 deletion packages/form/addon/gql/fragments/field.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ fragment SimpleAnswer on Answer {
listValue: value
}
... on FilesAnswer {
fileValue: value {
filesValue: value {
id
uploadUrl
downloadUrl
Expand Down
7 changes: 0 additions & 7 deletions packages/form/addon/gql/mutations/remove-answer.graphql

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ module("Integration | Component | cf-field/input/file", function (hooks) {
})();

this.onSave = (name) => ({
fileValue: { uploadUrl: `/minio/upload/${name}` },
filesValue: { uploadUrl: `/minio/upload/${name}` },
});

const payload_good = new File(["test"], "good.txt", { type: "text/plain" });
Expand Down
9 changes: 7 additions & 2 deletions packages/testing/addon/mirage-graphql/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -1407,7 +1407,7 @@ type FilesAnswer implements Answer & Node {
"""
id: ID!
question: Question!
value: File!
value: [File]!
meta: GenericScalar!
file: File
}
Expand Down Expand Up @@ -2844,10 +2844,15 @@ input SaveDocumentFilesAnswerInput {
question: ID!
document: ID!
meta: JSONString
value: String
value: SaveFile
clientMutationId: String
}

input SaveFile {
id: String
name: String
}

type SaveDocumentFilesAnswerPayload {
answer: Answer
clientMutationId: String
Expand Down

0 comments on commit aed8ca1

Please sign in to comment.