Skip to content

Commit

Permalink
Merge pull request #1482 from FlowFuse/wysiwyg-add-rem-spacer
Browse files Browse the repository at this point in the history
New Feature: wysiwyg design time editing - add and remove spacer widgets
  • Loading branch information
Steve-Mcl authored Nov 19, 2024
2 parents 031ee68 + e92899d commit 105b2c4
Show file tree
Hide file tree
Showing 10 changed files with 632 additions and 188 deletions.
105 changes: 79 additions & 26 deletions nodes/config/ui_base.js
Original file line number Diff line number Diff line change
Expand Up @@ -722,7 +722,7 @@ module.exports = function (RED) {
// any widgets we hard-code into our front end (e.g ui-notification for connection alerts) will start with ui-
// Node-RED built nodes will be a random UUID
if (!wNode && !id.startsWith('ui-')) {
console.log('widget does not exist any more')
console.log('widget does not exist in the runtime', id) // TODO: Handle this better for edit-time added nodes (e.g. ui-spacer)
return // widget does not exist any more (e.g. deleted from NR and deployed BUT the ui page was not refreshed)
}
async function handler () {
Expand Down Expand Up @@ -1138,12 +1138,16 @@ module.exports = function (RED) {
const changes = req.body.changes || {}
const editKey = req.body.key
const groups = changes.groups || []
const widgets = changes.widgets || []
const allWidgets = (changes.widgets || [])
const updatedWidgets = allWidgets.filter(w => !w.__DB2_ADD_WIDGET && !w.__DB2_REMOVE_WIDGET)
const addedWidgets = allWidgets.filter(w => !!w.__DB2_ADD_WIDGET).map(w => { delete w.__DB2_ADD_WIDGET; return w })
const removedWidgets = allWidgets.filter(w => !!w.__DB2_REMOVE_WIDGET).map(w => { delete w.__DB2_REMOVE_WIDGET; return w })

console.log(changes, editKey, dashboardId)
const baseNode = RED.nodes.getNode(dashboardId)

// validity checks
if (groups.length === 0 && widgets.length === 0) {
if (groups.length === 0 && allWidgets.length === 0) {
// this could be a 200 but since the group data might be missing due to
// a bug or regression, we'll return a 400 and let the user know
// there were no changes provided.
Expand All @@ -1166,14 +1170,30 @@ module.exports = function (RED) {
return res.status(400).json({ error: 'Invalid page id' })
}
}
for (const key in widgets) {
const groupWidgets = widgets[key]
for (const modified of groupWidgets) {
// ensure widget exists
const widget = baseNode.ui.widgets.get(modified.id)
if (!widget) {
return res.status(400).json({ error: 'Widget not found' })
}

for (const widget of updatedWidgets) {
const existingWidget = baseNode.ui.widgets.get(widget.id)
if (!existingWidget) {
return res.status(400).json({ error: 'Widget not found' })
}
}

for (const added of addedWidgets) {
// for now, only ui-spacer is supported
if (added.type !== 'ui-spacer') {
return res.status(400).json({ error: 'Cannot add this kind of widget' })
}

// check if the widget is being added to a valid group
const group = baseNode.ui.groups.get(added.group)
if (!group) {
return res.status(400).json({ error: 'Invalid group id' })
}
}
for (const removed of removedWidgets) {
// for now, only ui-spacer is supported
if (removed.type !== 'ui-spacer') {
return res.status(400).json({ error: 'Cannot remove this kind of widget' })
}
}

Expand Down Expand Up @@ -1237,21 +1257,54 @@ module.exports = function (RED) {
changeResult.push(applyIfDifferent(current, modified, 'width'))
changeResult.push(applyIfDifferent(current, modified, 'order'))
}
for (const widgetKey in widgets) {
const groupWidgets = widgets[widgetKey]
for (const modified of groupWidgets) {
const current = flows.find(n => n.id === modified.id)
if (!current) {
// widget not found in current flows! integrity of data suspect! Has flows changed on the server?
return res.status(400).json({ error: 'Widget not found', code: 'WIDGET_NOT_FOUND' })
}
if (modified.group !== current.group) {
// integrity of data suspect! Has flow changed on the server?
return res.status(400).json({ error: 'Invalid group id', code: 'INVALID_GROUP_ID' })
}
changeResult.push(applyIfDifferent(current, modified, 'order'))
changeResult.push(applyIfDifferent(current, modified, 'width'))
changeResult.push(applyIfDifferent(current, modified, 'height'))
// scan through the widgets and apply changes (if any)
for (const modified of updatedWidgets) {
const current = flows.find(n => n.id === modified.id)
if (!current) {
// widget not found in current flows! integrity of data suspect! Has flows changed on the server?
return res.status(400).json({ error: 'Widget not found', code: 'WIDGET_NOT_FOUND' })
}
if (modified.group !== current.group) {
// integrity of data suspect! Has flow changed on the server?
// Currently we dont support moving widgets between groups
return res.status(400).json({ error: 'Invalid group id', code: 'INVALID_GROUP_ID' })
}
changeResult.push(applyIfDifferent(current, modified, 'order'))
changeResult.push(applyIfDifferent(current, modified, 'width'))
changeResult.push(applyIfDifferent(current, modified, 'height'))
}

// scan through the added widgets
for (const added of addedWidgets) {
const current = flows.find(n => n.id === added.id)
if (current) {
// widget already exists in current flows! integrity of data suspect! Has flows changed on the server?
return res.status(400).json({ error: 'Widget already exists', code: 'WIDGET_ALREADY_EXISTS' })
}
// sanitize the added widget (NOTE: only ui-spacer is supported for now & these are the only properties we care about)
const newWidget = {
id: added.id,
type: added.type,
group: added.group,
name: added.name || '',
order: added.order ?? 0,
width: added.width ?? 1,
height: added.height ?? 1,
className: added.className || ''
}
flows.push(newWidget)
changeResult.push(true)
}
for (const removed of removedWidgets) {
const current = flows.find(n => n.id === removed.id)
if (!current) {
// widget not found in current flows! integrity of data suspect! Has flows changed on the server?
return res.status(400).json({ error: 'Widget not found', code: 'WIDGET_NOT_FOUND' })
}
const index = flows.indexOf(current)
if (index > -1) {
flows.splice(index, 1)
changeResult.push(true)
}
}
if (changeResult.length === 0 || !changeResult.includes(true)) {
Expand Down
92 changes: 4 additions & 88 deletions ui/src/EditTracking.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ const state = reactive({
editPage: '',
editMode: false,
editorPath: '', // the custom httpAdminRoot path for the NR editor
isTrackingEdits: false,
originalGroups: [],
originalWidgets: []
isTrackingEdits: false
})

// Methods
Expand All @@ -28,108 +26,26 @@ function initialise (editKey, editPage, editorPath) {
/**
* Start tracking edits
*/
function startEditTracking (groups, widgets) {
function startEditTracking () {
state.isTrackingEdits = true
updateEditTracking(groups, widgets)
}

/**
* Stop tracking edits, clear editKey/editPage & exit edit mode
*/
function exitEditMode () {
function endEditMode () {
state.editKey = ''
state.editPage = ''
state.editMode = false
state.isTrackingEdits = false
state.initialised = false
state.originalGroups = []
state.originalWidgets = []
}

/**
* Update the original groups with the current groups
*/
function updateEditTracking (groups, widgets) {
if (typeof groups !== 'undefined') {
state.originalGroups = JSON.parse(JSON.stringify(groups))
}
if (typeof widgets !== 'undefined') {
// only store the id, props and layout of each widget (that's all we need for comparison)
const groupIds = Object.keys(widgets)
const partials = {}
for (let i = 0; i < groupIds.length; i++) {
const groupId = groupIds[i]
const groupWidgets = widgets[groupId]
const partialWidgets = groupWidgets.map((w) => {
return {
id: w.id,
props: w.props,
layout: w.layout
}
})
partials[groupId] = partialWidgets
}
state.originalWidgets = JSON.parse(JSON.stringify(partials))
}
}

// RO computed props
const editKey = computed(() => state.editKey)
const editPage = computed(() => state.editPage)
const editMode = computed(() => !!state.editKey && !!state.editPage)
const editorPath = computed(() => state.editorPath)
const originalGroups = computed(() => state.originalGroups)
const originalWidgets = computed(() => state.originalWidgets)
const isTrackingEdits = computed(() => state.isTrackingEdits)

const groupPropertiesToCheck = [
(original, current) => +original.width === +current.width,
(original, current) => +original.height === +current.height,
(original, current) => +original.order === +current.order
]

const widgetPropertiesToCheck = [
(original, current) => +original.layout?.width === +current.layout?.width,
(original, current) => +original.layout?.height === +current.layout?.height,
(original, current) => +original.layout?.order === +current.layout?.order,
(original, current) => +original.props?.width === +current.props?.width,
(original, current) => +original.props?.height === +current.props?.height,
(original, current) => +original.props?.order === +current.props?.order
]

function isDirty (groups, widgets) {
console.log('isDirty', groups, widgets)
const originalGroups = state.originalGroups || []
// scan through each group and revert changes

for (let i = 0; i < originalGroups.length; i++) {
const originalGroup = originalGroups[i]
const currentGroup = groups?.find(group => group.id === originalGroup.id)
if (!currentGroup) {
console.warn('Group not found in pageGroups - as we do not currently support adding/removing groups, this should not happen!')
return true
}
// test group properties
if (groupPropertiesToCheck.some(check => !check(originalGroup, currentGroup))) {
return true
}

// test widgets belonging to this group
const originalWidgetValues = state.originalWidgets?.[originalGroup.id] || []
const currentWidgets = widgets?.[originalGroup.id] || []
for (let j = 0; j < originalWidgetValues.length; j++) {
const originalWidget = originalWidgetValues[j]
const currentWidget = currentWidgets.find(widget => widget.id === originalWidget.id)
if (!currentWidget) {
console.warn('Widget not found in pageWidgets - as we do not currently support adding/removing widgets, this should not happen!')
return true
}
if (widgetPropertiesToCheck.some(check => !check(originalWidget, currentWidget))) {
return true
}
}
}
return false
}

export { editKey, editMode, editPage, editorPath, originalGroups, originalWidgets, isDirty, isTrackingEdits, initialise, exitEditMode, startEditTracking, updateEditTracking }
export { editKey, editMode, editPage, editorPath, isTrackingEdits, initialise, startEditTracking, endEditMode }
20 changes: 12 additions & 8 deletions ui/src/layouts/Flex.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
{{ g.name }}
</template>
<template #text>
<widget-group :group="g" :index="$index" :widgets="groupWidgets(g.id)" :resizable="editMode" :group-dragging="groupDragging.active" @resize="onGroupResize" />
<widget-group :group="g" :index="$index" :widgets="groupWidgets(g.id)" :resizable="editMode" :group-dragging="groupDragging.active" @resize="onGroupResize" @widget-added="updateEditStateObjects" @widget-removed="updateEditStateObjects" @refresh-state-from-store="updateEditStateObjects" />
</template>
</v-card>
</div>
Expand Down Expand Up @@ -117,12 +117,7 @@ export default {
mounted () {
console.log('flex layout mounted')
if (this.editMode) { // mixin property
this.pageGroups = this.getPageGroups()
const pageGroupWidgets = {}
for (const group of this.pageGroups) {
pageGroupWidgets[group.id] = this.getGroupWidgets(group.id)
}
this.pageGroupWidgets = pageGroupWidgets
this.updateEditStateObjects()
this.initializeEditTracking() // Mixin method
}
},
Expand Down Expand Up @@ -225,7 +220,7 @@ export default {
},
discardEdits () {
this.revertEdits() // Mixin method
this.pageGroups = this.getPageGroups()
this.updateEditStateObjects()
},
async leaveEditMode () {
let leave = true
Expand Down Expand Up @@ -253,6 +248,15 @@ export default {
return
}
this.pageGroups[opts.index].width = opts.width
},
updateEditStateObjects () {
console.log('updateEditStateObjects')
this.pageGroups = this.getPageGroups()
const pageGroupWidgets = {}
for (const group of this.pageGroups) {
pageGroupWidgets[group.id] = this.getGroupWidgets(group.id)
}
this.pageGroupWidgets = pageGroupWidgets
}
}
}
Expand Down
20 changes: 12 additions & 8 deletions ui/src/layouts/Grid.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
{{ g.name }}
</template>
<template #text>
<widget-group :group="g" :index="$index" :widgets="groupWidgets(g.id)" :resizable="editMode" :group-dragging="groupDragging.active" @resize="onGroupResize" />
<widget-group :group="g" :index="$index" :widgets="groupWidgets(g.id)" :resizable="editMode" :group-dragging="groupDragging.active" @resize="onGroupResize" @widget-added="updateEditStateObjects" @widget-removed="updateEditStateObjects" @refresh-state-from-store="updateEditStateObjects" />
</template>
</v-card>
</div>
Expand Down Expand Up @@ -117,12 +117,7 @@ export default {
mounted () {
console.log('grid layout mounted')
if (this.editMode) { // mixin property
this.pageGroups = this.getPageGroups()
const pageGroupWidgets = {}
for (const group of this.pageGroups) {
pageGroupWidgets[group.id] = this.getGroupWidgets(group.id)
}
this.pageGroupWidgets = pageGroupWidgets
this.updateEditStateObjects()
this.initializeEditTracking() // Mixin method
}
},
Expand Down Expand Up @@ -225,7 +220,7 @@ export default {
},
discardEdits () {
this.revertEdits() // Mixin method
this.pageGroups = this.getPageGroups()
this.updateEditStateObjects()
},
async leaveEditMode () {
let leave = true
Expand Down Expand Up @@ -253,6 +248,15 @@ export default {
return
}
this.pageGroups[opts.index].width = opts.width
},
updateEditStateObjects () {
console.log('Updating edit state objects')
this.pageGroups = this.getPageGroups()
const pageGroupWidgets = {}
for (const group of this.pageGroups) {
pageGroupWidgets[group.id] = this.getGroupWidgets(group.id)
}
this.pageGroupWidgets = pageGroupWidgets
}
}
}
Expand Down
Loading

0 comments on commit 105b2c4

Please sign in to comment.