Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Feature: wysiwyg design time editing - add and remove spacer widgets #1482

Merged
merged 1 commit into from
Nov 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading