Skip to content

Commit

Permalink
Widget: File Upload
Browse files Browse the repository at this point in the history
  • Loading branch information
joepavitt committed Jun 15, 2024
1 parent 2cb8ca9 commit d9c728c
Show file tree
Hide file tree
Showing 6 changed files with 313 additions and 1 deletion.
125 changes: 125 additions & 0 deletions nodes/widgets/ui_file_input.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<script type="text/javascript">
(function () {
function hasProperty (obj, prop) {
return Object.prototype.hasOwnProperty.call(obj, prop)
}
RED.nodes.registerType('ui-file-input', {
category: RED._('@flowfuse/node-red-dashboard/ui-base:ui-base.label.category'),
color: RED._('@flowfuse/node-red-dashboard/ui-base:ui-base.colors.light'),
defaults: {
group: { type: 'ui-group', required: true },
name: { value: '' },
order: { value: 0 },
width: {
value: 0,
validate: function (v) {
const width = v || 0
const currentGroup = $('#node-input-group').val() || this.group
const groupNode = RED.nodes.node(currentGroup)
const valid = !groupNode || +width <= +groupNode.width
$('#node-input-size').toggleClass('input-error', !valid)
return valid
}
},
height: { value: 0 },
topic: { value: 'topic', validate: (hasProperty(RED.validators, 'typedInput') ? RED.validators.typedInput('topicType') : function (v) { return true }) },
topicType: { value: 'msg' },
label: { value: 'File Input' },
icon: { value: 'paperclip' },
showFileSize: { value: false },
allowMultiple: { value: false },
accept: { value: '' },
className: { value: '' }
},
inputs: 1,
outputs: 1,
icon: 'font-awesome/fa-upload',
paletteLabel: 'file input',
oneditprepare: function () {
// if this groups parent is a subflow template, set the node-config-input-width and node-config-input-height up
// as typedInputs and hide the elementSizer (as it doesn't make sense for subflow templates)
if (RED.nodes.subflow(this.z)) {
// change inputs from hidden to text & display them
$('#node-input-width').attr('type', 'text')
$('#node-input-height').attr('type', 'text')
$('div.form-row.nr-db-ui-element-sizer-row').hide()
$('div.form-row.nr-db-ui-manual-size-row').show()
} else {
// not in a subflow, use the elementSizer
$('div.form-row.nr-db-ui-element-sizer-row').show()
$('div.form-row.nr-db-ui-manual-size-row').hide()
$('#node-input-size').elementSizer({
width: '#node-input-width',
height: '#node-input-height',
group: '#node-input-group'
})
}
// topic
$('#node-input-topic').typedInput({
default: 'str',
typeField: $('#node-input-topicType'),
types: ['str', 'msg', 'flow', 'global']
})
},
label: function () {
return this.name || this.label || 'file input'
},
labelStyle: function () { return this.name ? 'node_label_italic' : '' }
})
})()
</script>

<script type="text/html" data-template-name="ui-file-input">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name">
</div>
<div class="form-row">
<label for="node-input-group"><i class="fa fa-table"></i> Group</label>
<input type="text" id="node-input-group">
</div>
<div class="form-row">
<label><i class="fa fa-object-group"></i> Size</label>
<input type="hidden" id="node-input-width">
<input type="hidden" id="node-input-height">
<button class="editor-button" id="node-input-size"></button>
</div>
<div class="form-row">
<label for="node-input-label"><i class="fa fa-i-cursor"></i> Label</label>
<input type="text" id="node-input-label">
</div>
<div class="form-row">
<label for="node-input-icon"><i class="fa fa-picture-o"></i> Icon</label>
<input type="text" id="node-input-icon">
</div>
<div class="form-row">
<label for="node-input-accept"><i class="fa fa-paperclip"></i> Accept</label>
<input type="text" id="node-input-accept">
</div>
<div class="form-row" style="padding-left: 25px;">
<input type="checkbox" checked id="node-input-allowMultiple" style="display:inline-block; width:auto; vertical-align:top;">
<label style="width:auto" for="node-input-allowMultiple"> Allow Multiple</label>
</div>
<div class="form-row" style="padding-left: 25px;">
<input type="checkbox" checked id="node-input-showFileSize" style="display:inline-block; width:auto; vertical-align:top;">
<label style="width:auto" for="node-input-showFileSize"> Show File Size</label>
</div>
<div class="form-row">
<label for="node-input-className"><i class="fa fa-code"></i> Class</label>
<div style="display: inline;">
<input style="width: 70%;" type="text" id="node-input-className" placeholder="Optional CSS class name(s)" style="flex-grow: 1;">
<a
data-html="true"
title="Dynamic Property: Send msg.class to append new classes to this widget. NOTE: classes set at runtime will be applied in addition to any class(es) set in the nodes class field."
class="red-ui-button ui-node-popover-title"
style="margin-left: 4px; cursor: help; font-size: 0.625rem; border-radius: 50%; width: 24px; height: 24px; display: inline-flex; justify-content: center; align-items: center;">
<i style="font-family: ui-serif;">fx</i>
</a>
</div>
</div>
<div class="form-row" style="padding-left: 25px;">
<label for="node-input-topic" style="margin-right:-25px">Topic</label>
<input type="text" id="node-input-topic">
<input type="hidden" id="node-input-topicType">
</div>
</script>
29 changes: 29 additions & 0 deletions nodes/widgets/ui_file_input.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// const datastore = require('../store/data.js')

module.exports = function (RED) {
function FileInputNode (config) {
const node = this

// create node in Node-RED
RED.nodes.createNode(this, config)

// this ndoe need to store content/value from UI
node.value = null

// which group are we rendering this widget
const group = RED.nodes.getNode(config.group)

const evts = {
onAction: true
}

// inform the dashboard UI that we are adding this node
group.register(node, config, evts)

node.on('close', async function (done) {
done()
})
}

RED.nodes.registerType('ui-file-input', FileInputNode)
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@
"ui-theme": "nodes/config/ui_theme.js",
"ui-form": "nodes/widgets/ui_form.js",
"ui-text-input": "nodes/widgets/ui_text_input.js",
"ui-file-input": "nodes/widgets/ui_file_input.js",
"ui-button": "nodes/widgets/ui_button.js",
"ui-button-group": "nodes/widgets/ui_button_group.js",
"ui-dropdown": "nodes/widgets/ui_dropdown.js",
Expand Down
14 changes: 13 additions & 1 deletion ui/src/stylesheets/common.css
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
/* main */
--nrdb-main-padding: 12px;
/* widget sizing */
--widget-row-height: 42px;
--widget-row-height: 48px;
}

body {
Expand Down Expand Up @@ -60,6 +60,18 @@ main {
fill: #bbb;
}

/**
* Anchor
*/

.nrdb-anchor {
cursor: pointer;
color: rgb(var(--v-theme-primary));
}
.nrdb-anchor:hover {
color: rgb(var(--v-theme-primary-darken-1));
}

/**
* Placeholder
*/
Expand Down
3 changes: 3 additions & 0 deletions ui/src/widgets/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import UIChart from './ui-chart/UIChart.vue'
import UIControl from './ui-control/UIControl.vue'
import UIDropdown from './ui-dropdown/UIDropdown.vue'
import UIEvent from './ui-event/UIEvent.vue'
import UIFileInput from './ui-file-input/UIFileInput.vue'
import UIForm from './ui-form/UIForm.vue'
import UIGauge from './ui-gauge/UIGauge.vue'
import UIMarkdown from './ui-markdown/UIMarkdown.vue'
Expand All @@ -24,6 +25,7 @@ export {
UIControl,
UIDropdown,
UIEvent,
UIFileInput,
UIForm,
UIGauge,
UIMarkdown,
Expand All @@ -48,6 +50,7 @@ export default {
'ui-control': UIControl,
'ui-dropdown': UIDropdown,
'ui-event': UIEvent,
'ui-file-input': UIFileInput,
'ui-form': UIForm,
'ui-gauge': UIGauge,
'ui-markdown': UIMarkdown,
Expand Down
142 changes: 142 additions & 0 deletions ui/src/widgets/ui-file-input/UIFileInput.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
<template>
<div class="nrdb-ui-file-input">
<v-file-input
v-if="!uploading && !uploaded"
v-model="files"
density="compact"
:disabled="!state.enabled"
:label="label" :prepend-icon="icon"
:accept="accept"
:show-size="showFileSize"
variant="outlined"
hide-details="auto"
/>
<div v-else-if="!uploaded" class="nrdb-ui-file-input--progress">
<v-progress-linear :model-value="progress" :height="24">
<template #default="{ value }">
<strong>{{ Math.ceil(value) }}%</strong>
</template>
</v-progress-linear>
</div>
<div v-else class="nrdb-ui-file-input--uploaded">
<v-icon icon="mdi-check-circle" color="success" />
<label class="v-label">File Uploaded!</label>
<label class="v-label" @click="reset()">
<a class="nrdb-anchor">Upload Another File</a>
</label>
</div>
<v-btn variant="flat" :disabled="!files || uploading || uploaded" @click="upload(files)">
{{ uploading ? 'Uploading...' : 'Upload' }}
</v-btn>
</div>
</template>

<script>
import { useDataTracker } from '../data-tracker.mjs' // eslint-disable-line import/order
import { mapState } from 'vuex' // eslint-disable-line import/order
export default {
name: 'DBUIFileInput',
inject: ['$socket'],
props: {
id: { type: String, required: true },
props: { type: Object, default: () => ({}) },
state: { type: Object, default: () => ({}) }
},
setup (props) {
useDataTracker(props.id)
},
data () {
return {
files: null,
uploading: false,
uploaded: false,
progress: 0
}
},
computed: {
...mapState('data', ['messages']),
icon: function () {
const icon = this.props.icon
return icon ? 'mdi-' + icon.replace(/^mdi-/, '') : null
},
accept: function () {
return this.props.accept
},
label: function () {
return this.props.label
},
showFileSize: function () {
return this.props.showFileSize
},
multiple: function () {
return this.props.multiple
}
},
methods: {
reset: function () {
this.files = null
this.uploading = false
this.uploaded = false
this.progress = 0
},
upload: function (file) {
if (file && !this.multiple) {
// Create a FileReader instance to read the file
const reader = new FileReader()
// When the file is read, send it to Node-RED
reader.onload = () => {
// Prepare the payload to send
const msg = {
payload: file, // File content
file: {
name: file.name, // File name
size: file.size, // File size
type: file.type // File type
}
}
this.uploading = false
this.uploaded = true
this.send(msg)
}
// Track progress of file reading
reader.onprogress = (event) => {
this.progress = event.loaded // Update progress
}
this.uploading = true
// readAsText alternative?
reader.readAsArrayBuffer(file)
}
},
send (msg) {
this.$socket.emit('widget-send', this.id, msg)
}
}
}
</script>

<style>
.nrdb-ui-file-input {
display: flex;
gap: 12px;
}
.nrdb-ui-file-input--progress {
width: 100%;
display: flex;
align-items: center;
}
.nrdb-ui-file-input--uploaded {
flex-grow: 1;
display: flex;
align-items: center;
justify-content: flex-start;
gap: 6px;
height: var(--widget-row-height);
}
</style>

0 comments on commit d9c728c

Please sign in to comment.