diff --git a/app/edge/views/edge-stacks/createEdgeStackView/create-edge-stack-view.controller.js b/app/edge/views/edge-stacks/createEdgeStackView/create-edge-stack-view.controller.js index db92bb610..2804df020 100644 --- a/app/edge/views/edge-stacks/createEdgeStackView/create-edge-stack-view.controller.js +++ b/app/edge/views/edge-stacks/createEdgeStackView/create-edge-stack-view.controller.js @@ -213,6 +213,11 @@ export default class CreateEdgeStackViewController { } formIsInvalid() { - return this.form.$invalid || !this.formValues.Groups.length || (['template', 'editor'].includes(this.state.Method) && !this.formValues.StackFileContent); + return ( + this.form.$invalid || + !this.formValues.Groups.length || + (['template', 'editor'].includes(this.state.Method) && !this.formValues.StackFileContent) || + ('upload' === this.state.Method && !this.formValues.StackFile) + ); } } diff --git a/app/edge/views/edge-stacks/createEdgeStackView/docker-compose-form/docker-compose-form.controller.js b/app/edge/views/edge-stacks/createEdgeStackView/docker-compose-form/docker-compose-form.controller.js index 25ee41886..9b967cc22 100644 --- a/app/edge/views/edge-stacks/createEdgeStackView/docker-compose-form/docker-compose-form.controller.js +++ b/app/edge/views/edge-stacks/createEdgeStackView/docker-compose-form/docker-compose-form.controller.js @@ -47,7 +47,9 @@ class DockerComposeFormController { } onChangeFile(value) { - this.formValues.StackFile = value; + return this.$async(async () => { + this.formValues.StackFile = value; + }); } async $onInit() { diff --git a/app/edge/views/edge-stacks/createEdgeStackView/kube-manifest-form/kube-manifest-form.controller.js b/app/edge/views/edge-stacks/createEdgeStackView/kube-manifest-form/kube-manifest-form.controller.js index 6fee23409..e43588682 100644 --- a/app/edge/views/edge-stacks/createEdgeStackView/kube-manifest-form/kube-manifest-form.controller.js +++ b/app/edge/views/edge-stacks/createEdgeStackView/kube-manifest-form/kube-manifest-form.controller.js @@ -1,6 +1,8 @@ class KubeManifestFormController { /* @ngInject */ - constructor() { + constructor($async) { + Object.assign(this, { $async }); + this.methodOptions = [ { id: 'method_editor', icon: 'fa fa-edit', label: 'Web editor', description: 'Use our Web editor', value: 'editor' }, { id: 'method_upload', icon: 'fa fa-upload', label: 'Upload', description: 'Upload from your computer', value: 'upload' }, @@ -23,7 +25,9 @@ class KubeManifestFormController { } onChangeFile(value) { - this.formValues.StackFile = value; + return this.$async(async () => { + this.formValues.StackFile = value; + }); } onChangeMethod(method) { diff --git a/app/kubernetes/custom-templates/kube-create-custom-template-view/kube-create-custom-template-view.controller.js b/app/kubernetes/custom-templates/kube-create-custom-template-view/kube-create-custom-template-view.controller.js index a724b5e7d..0e2254e4f 100644 --- a/app/kubernetes/custom-templates/kube-create-custom-template-view/kube-create-custom-template-view.controller.js +++ b/app/kubernetes/custom-templates/kube-create-custom-template-view/kube-create-custom-template-view.controller.js @@ -46,7 +46,9 @@ class KubeCreateCustomTemplateViewController { } onChangeFile(file) { - this.formValues.File = file; + return this.$async(async () => { + this.formValues.File = file; + }); } async createCustomTemplate() { diff --git a/app/portainer/components/form-components/FileUpload/FileUploadField.module.css b/app/portainer/components/form-components/FileUpload/FileUploadField.module.css new file mode 100644 index 000000000..5ceb85832 --- /dev/null +++ b/app/portainer/components/form-components/FileUpload/FileUploadField.module.css @@ -0,0 +1,3 @@ +.file-input { + display: none !important; +} diff --git a/app/portainer/components/form-components/FileUpload/FileUploadField.stories.tsx b/app/portainer/components/form-components/FileUpload/FileUploadField.stories.tsx new file mode 100644 index 000000000..9e1e610d3 --- /dev/null +++ b/app/portainer/components/form-components/FileUpload/FileUploadField.stories.tsx @@ -0,0 +1,24 @@ +import { Meta } from '@storybook/react'; +import { useState } from 'react'; + +import { FileUploadField } from './FileUploadField'; + +export default { + component: FileUploadField, + title: 'Components/Buttons/FileUploadField', +} as Meta; + +interface Args { + title: string; +} + +export function Example({ title }: Args) { + const [value, setValue] = useState(); + function onChange(value: File) { + if (value) { + setValue(value); + } + } + + return ; +} diff --git a/app/portainer/components/form-components/FileUpload/FileUploadField.test.tsx b/app/portainer/components/form-components/FileUpload/FileUploadField.test.tsx new file mode 100644 index 000000000..dca222b94 --- /dev/null +++ b/app/portainer/components/form-components/FileUpload/FileUploadField.test.tsx @@ -0,0 +1,24 @@ +import { fireEvent, render } from '@/react-tools/test-utils'; + +import { FileUploadField } from './FileUploadField'; + +test('render should make the file button clickable and fire onChange event after click', async () => { + const onClick = jest.fn(); + const { findByText, findByLabelText } = render( + + ); + + const button = await findByText('test button'); + expect(button).toBeVisible(); + + const input = await findByLabelText('file-input'); + expect(input).not.toBeNull(); + + const mockFile = new File([], 'file.txt'); + if (input) { + fireEvent.change(input, { + target: { files: [mockFile] }, + }); + } + expect(onClick).toHaveBeenCalledWith(mockFile); +}); diff --git a/app/portainer/components/form-components/FileUpload/FileUploadField.tsx b/app/portainer/components/form-components/FileUpload/FileUploadField.tsx new file mode 100644 index 000000000..ac5f21db1 --- /dev/null +++ b/app/portainer/components/form-components/FileUpload/FileUploadField.tsx @@ -0,0 +1,65 @@ +import { ChangeEvent, createRef } from 'react'; + +import { r2a } from '@/react-tools/react2angular'; +import { Button } from '@/portainer/components/Button'; + +import styles from './FileUploadField.module.css'; + +export interface Props { + onChange(value: File): void; + value?: File; + title?: string; + required?: boolean; +} + +export function FileUploadField({ + onChange, + value, + title = 'Select a file', + required = false, +}: Props) { + const fileRef = createRef(); + + return ( +
+ + + + + {value ? ( + value.name + ) : ( +