Skip to content

Commit

Permalink
Merge pull request #356 from valb3r/feature/FBP-289-unique-id-validation
Browse files Browse the repository at this point in the history
Feature/fbp 289 unique id validation
  • Loading branch information
valb3r authored Jul 2, 2023
2 parents bd10e37 + df604ae commit 99106b9
Show file tree
Hide file tree
Showing 7 changed files with 164 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import javax.swing.Icon
import javax.swing.JButton
import javax.swing.JCheckBox
import javax.swing.JTable
import javax.swing.JTextField
import javax.swing.SwingConstants
import javax.swing.plaf.basic.BasicArrowButton
import javax.swing.table.TableColumn
Expand Down Expand Up @@ -194,6 +195,7 @@ abstract class BaseUiTest {
protected val editorFactory = { id: BpmnElementId, type: PropertyType, value: String -> textFieldsConstructed.computeIfAbsent(Pair(id, type)) {
val res = mock<TextValueAccessor>()
whenever(res.text).thenReturn(value)
whenever(res.component).thenReturn(JTextField())
return@computeIfAbsent res
} }
protected val multiLineEditorFactory = { id: BpmnElementId, type: PropertyType, value: String -> multiLineTextFieldsConstructed.computeIfAbsent(Pair(id, type)) {
Expand Down Expand Up @@ -327,7 +329,7 @@ abstract class BaseUiTest {
bounds = BoundsElement(intermediateX, intermediateY, taskSize, taskSize)
)
updateEventsRegistry(project).addObjectEvent(
BpmnShapeObjectAddedEvent(WithParentId(basicProcess.process.id, task), shape, PropertyTable(mutableMapOf(PropertyType.ID to mutableListOf(Property(task.id)))))
BpmnShapeObjectAddedEvent(WithParentId(basicProcess.process.id, task), shape, PropertyTable(mutableMapOf(PropertyType.ID to mutableListOf(Property(task.id.id)))))
)

return task.id
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import com.valb3r.bpmn.intellij.plugin.core.newelements.newElementsFactory
import com.valb3r.bpmn.intellij.plugin.core.state.currentStateProvider
import com.valb3r.bpmn.intellij.plugin.core.ui.components.FirstLastColumnReadOnlyModel
import org.jetbrains.annotations.VisibleForTesting
import java.io.Serializable
import java.util.*
import javax.swing.*
import javax.swing.plaf.basic.BasicArrowButton
Expand Down Expand Up @@ -112,53 +113,55 @@ class PropertiesVisualizer(
model: FirstLastColumnReadOnlyModel,
state: Map<BpmnElementId, PropertyTable>,
bpmnElementId: BpmnElementId,
controls: List<Pair<PropertyType, Property>>,
properties: List<Pair<PropertyType, Property>>,
filter: RowExpansionFilter,
sorter: TableRowSorter<TableModel>,
elemsToExpand: Set<ElementIndex>): Set<BasicArrowButton> {
val seenIndexes = mutableSetOf<ElementIndex>()
val buttonsToClick = mutableSetOf<BasicArrowButton>()
for (control in controls) {
if (control.first.caption.isEmpty()) {
for (property in properties) {
if (property.first.caption.isEmpty()) {
continue
}
val groupType = control.first.group?.lastOrNull()
val isExpandButton = control.first.name == groupType?.actionResult?.propertyType
val isAlwaysVisible = control.first.group?.size == 1 && isExpandButton
val groupType = property.first.group?.lastOrNull()
val isExpandButton = property.first.name == groupType?.actionResult?.propertyType
val isAlwaysVisible = property.first.group?.size == 1 && isExpandButton
val controlGroupIndex = ElementIndex(
if (isExpandButton && control.first.isNestedProperty()) control.first.group?.getOrNull(control.first.group!!.size - 2) else groupType,
control.second.index?.take(max(0, control.first.group!!.size - if (isExpandButton) 1 else 0))?.joinToString() ?: ""
if (isExpandButton && property.first.isNestedProperty()) property.first.group?.getOrNull(property.first.group!!.size - 2) else groupType,
property.second.index?.take(max(0, property.first.group!!.size - if (isExpandButton) 1 else 0))?.joinToString() ?: ""
)
val lengthInnerPad = control.second.index?.let {(it.size - 1) * 2} ?: 0
val lengthInnerPad = property.second.index?.let {(it.size - 1) * 2} ?: 0
val paddGroup = "".padStart(lengthInnerPad * 2)
if (null != groupType && isExpandButton && !seenIndexes.contains(controlGroupIndex) && groupType.createExpansionButton) {
addCurrentRowToCollapsedSectionIfNeeded(controlGroupIndex, filter, model, isAlwaysVisible)
model.addRow(arrayOf(
paddGroup + groupType.groupCaption,
buildButtonField(newElemsProvider, state, bpmnElementId, groupType, control.second.index?.dropLast(1) ?: listOf())
buildButtonField(newElemsProvider, state, bpmnElementId, groupType, property.second.index?.dropLast(1) ?: listOf())
))
seenIndexes.add(controlGroupIndex)
}

if (control.first.hideIfNullOrEmpty && (null == control.second.value || (control.second.value is String && (control.second.value as String).isBlank()))) {
if (property.first.hideIfNullOrEmpty && (null == property.second.value || (property.second.value is String && (property.second.value as String).isBlank()))) {
continue
}
val nestedGroupLength = (2 + (if(!isExpandButton) 2 else 0) - (if (groupType?.createExpansionButton == false) 2 else 0))
val padd = paddGroup + "".padStart(if(groupType == null) 0 else nestedGroupLength)
val caption = padd + control.first.caption
var row = when (control.first.valueType) {
STRING -> arrayOf(caption, buildTextField(state, bpmnElementId, control.first, control.second))
BOOLEAN -> arrayOf(caption, buildCheckboxField(state, bpmnElementId, control.first, control.second))
CLASS -> arrayOf(caption, buildClassField(state, bpmnElementId, control.first, control.second))
EXPRESSION -> arrayOf(caption, buildExpressionField(state, bpmnElementId, control.first, control.second))
ATTACHED_SEQUENCE_SELECT -> arrayOf(caption, buildDropDownSelectFieldForTargettedIds(state, bpmnElementId, control.first, control.second))
LIST_SELECT -> arrayOf(caption, buildDropDownSelect(state, bpmnElementId, control.first, control.second))
val caption = padd + property.first.caption
var row = when (property.first.valueType) {
STRING -> arrayOf(caption, buildTextField(state, bpmnElementId, property.first, property.second))
BOOLEAN -> arrayOf(caption, buildCheckboxField(state, bpmnElementId, property.first, property.second))
CLASS -> arrayOf(caption, buildClassField(state, bpmnElementId, property.first, property.second))
EXPRESSION -> arrayOf(caption, buildExpressionField(state, bpmnElementId, property.first, property.second))
ATTACHED_SEQUENCE_SELECT -> arrayOf(caption, buildDropDownSelectFieldForTargettedIds(state, bpmnElementId, property.first, property.second))
LIST_SELECT -> arrayOf(caption, buildDropDownSelect(state, bpmnElementId, property.first, property.second))
}

addVerifierIfAvailable(property, row)

if (isExpandButton) {
val controlExpandsGroupIndex = ElementIndex(
groupType,
control.second.index?.joinToString() ?: ""
property.second.index?.joinToString() ?: ""
)
val button = buildArrowExpansionButton(bpmnElementId, filter, controlExpandsGroupIndex, sorter)
row += button
Expand All @@ -175,6 +178,17 @@ class PropertiesVisualizer(
return buttonsToClick
}

private fun addVerifierIfAvailable(property: Pair<PropertyType, Property>, row: Array<Serializable>) {
val control = row[1]
if (property.first == PropertyType.ID && control is JTextField) {
control.inputVerifier = PropertyInputVerifier(property.second.value, "Non unique ID!") { c, initial ->
val currentValue = (c as JTextField).text
return@PropertyInputVerifier !currentStateProvider(project).currentState().allElementIds()
.contains(currentValue) || (initial == currentValue)
}
}
}

private fun addCurrentRowToCollapsedSectionIfNeeded(
controlGroupIndex: ElementIndex,
filter: RowExpansionFilter,
Expand All @@ -193,10 +207,21 @@ class PropertiesVisualizer(

private fun notifyDeFocusElement() {
// Fire de-focus to move changes to memory (Using order as ID property), component listeners doesn't seem to work with EditorTextField
listenersForCurrentView.toSortedMap().flatMap { it.value }.forEach { it() }
if (checkIsValidTableAndHidePopups()) {
listenersForCurrentView.toSortedMap().flatMap { it.value }.forEach { it() }
}
listenersForCurrentView.clear()
}

private fun checkIsValidTableAndHidePopups(): Boolean {
val allComponents = (0 until table.model.rowCount)
.flatMap { row -> (0 until table.model.columnCount).map { col -> table.model.getValueAt(row, col) } }
.filterIsInstance<JComponent>()
allComponents.map { it.inputVerifier }.filterIsInstance<PropertyInputVerifier>()
.forEach { it.hidePopupIfOpen() }
return allComponents.filter { null != it.inputVerifier }.all { it.inputVerifier.verify(it) }
}

private fun buildTextField(state: Map<BpmnElementId, PropertyTable>, bpmnElementId: BpmnElementId, type: PropertyType, value: Property): JComponent {
val fieldValue = extractString(value)
val field = if (type.multiline) multiLineExpandableTextFieldFactory.invoke(bpmnElementId, type, fieldValue) else textFieldFactory.invoke(bpmnElementId, type, fieldValue)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.valb3r.bpmn.intellij.plugin.core.properties

import com.intellij.ui.JBColor
import java.awt.Color
import javax.swing.*

class PropertyInputVerifier(
private val initialValue: Any?,
private val message: String,
private val verifier: (JComponent, Any?) -> Boolean
): InputVerifier() {

private var background: Color? = null
private var activePopup: Popup? = null

override fun verify(input: JComponent): Boolean {
return verifier(input, initialValue)
}

fun onStopEditing(input: JComponent): Boolean {
if (verifier(input, initialValue)) {
hidePopupIfOpen()
input.background = this.background
return true
}

input.background = JBColor.PINK
val popup = PopupFactory.getSharedInstance().getPopup(
input,
JLabel(message),
input.locationOnScreen.x,
input.locationOnScreen.y + input.height
)
hidePopupIfOpen()
popup.show()
this.activePopup = popup

val timer = Timer(5_000) { popup.hide() }
timer.isRepeats = false
timer.start()
return false
}

fun hidePopupIfOpen() {
activePopup?.hide()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ data class CurrentState(
return processDiagramId(processId)
}

fun allElementIds(): Set<String?> {
return elemPropertiesByStaticElementId.values.flatMap {
it.getAll(PropertyType.ID).map { it.value as String? }
}.toSet()
}

companion object {
fun processDiagramId(processId: BpmnElementId): DiagramElementId {
return DiagramElementId(processId.id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ package com.valb3r.bpmn.intellij.plugin.core.ui.components

import com.intellij.openapi.ui.ComboBox
import com.intellij.ui.EditorTextField
import com.intellij.ui.JBColor
import com.intellij.ui.components.JBCheckBox
import com.intellij.ui.components.JBTextArea
import com.intellij.ui.components.JBTextField
import com.intellij.ui.table.JBTable
import com.intellij.util.ui.AbstractTableCellEditor
import com.valb3r.bpmn.intellij.plugin.core.properties.PropertyInputVerifier
import com.valb3r.bpmn.intellij.plugin.core.settings.currentSettings
import java.awt.Component
import java.awt.Font
Expand Down Expand Up @@ -133,6 +135,17 @@ class JBTextFieldCellEditor(val field: JBTextField): AbstractTableCellEditor() {
override fun getCellEditorValue(): Any {
return field
}

override fun stopCellEditing(): Boolean {
val verifier = field.inputVerifier ?: return true
val result = verifier.verify(field)

if (verifier is PropertyInputVerifier) {
verifier.onStopEditing(field)
}

return result
}
}

class JBTextAreaCellEditor(val field: JBTextArea): AbstractTableCellEditor() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import java.awt.image.BufferedImage
import java.util.*
import javax.swing.JButton
import javax.swing.JCheckBox
import javax.swing.JTextField
import javax.swing.plaf.basic.BasicArrowButton
import javax.swing.table.DefaultTableModel
import javax.swing.table.TableRowSorter
Expand Down Expand Up @@ -1671,6 +1672,7 @@ internal class UiEditorLightE2ETest: BaseUiTest() {
is BasicArrowButton -> "BasicArrowButton"
is JButton -> it.text
is JCheckBox -> null
is JTextField -> null
else -> {
it
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.valb3r.bpmn.intellij.plugin

import com.nhaarman.mockitokotlin2.any
import com.nhaarman.mockitokotlin2.never
import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.whenever
import com.valb3r.bpmn.intellij.plugin.bpmn.api.info.PropertyType
import com.valb3r.bpmn.intellij.plugin.core.newelements.registerNewElementsFactory
import com.valb3r.bpmn.intellij.plugin.core.properties.propertiesVisualizer
import com.valb3r.bpmn.intellij.plugin.core.tests.BaseUiTest
import com.valb3r.bpmn.intellij.plugin.flowable.parser.FlowableObjectFactory
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import javax.swing.JTextField

internal class ValidationTest: BaseUiTest() {

@BeforeEach
fun prepare() {
registerNewElementsFactory(project, FlowableObjectFactory())
prepareTwoServiceTaskView()
}

@Test
fun `Invalid ID (duplicate) not propagated`() {
clickOnId(serviceTaskEndDiagramId)
val idField = textFieldsConstructed[Pair(serviceTaskEndBpmnId, PropertyType.ID)]!!
whenever(idField.text).thenReturn(serviceTaskStartBpmnId.id)
(idField.component as JTextField).text = serviceTaskStartBpmnId.id

propertiesVisualizer(project).clear()

verify(fileCommitter, never()).executeCommitAndGetHash(any(), any(), any(), any())
}

@Test
fun `Valid ID (not duplicate) propagated`() {
clickOnId(serviceTaskEndDiagramId)
val idField = textFieldsConstructed[Pair(serviceTaskEndBpmnId, PropertyType.ID)]!!
whenever(idField.text).thenReturn(serviceTaskStartBpmnId.id + '1')
(idField.component as JTextField).text = serviceTaskStartBpmnId.id + '1'

propertiesVisualizer(project).clear()

verify(fileCommitter).executeCommitAndGetHash(any(), any(), any(), any())
}
}

0 comments on commit 99106b9

Please sign in to comment.