diff --git a/app/app.js b/app/app.js index edfa6fa..29bd744 100644 --- a/app/app.js +++ b/app/app.js @@ -23,6 +23,13 @@ try { keyboard: { bindTo: document }, propertiesPanel: { parent: panelEl, + layout: { + groups: { + general: { + open: true + } + } + } }, additionalModules: [ spiffworkflow, @@ -206,7 +213,7 @@ bpmnModeler.on('import.parse.complete', event => { refs.forEach(ref => { const props = { id: ref.id, - name: ref.id ? typeof(ref.name) === 'undefined': ref.name, + name: ref.id ? typeof (ref.name) === 'undefined' : ref.name, }; let elem = bpmnModeler._moddle.create(desc, props); elem.$parent = ref.element; @@ -214,7 +221,10 @@ bpmnModeler.on('import.parse.complete', event => { }); }); -bpmnModeler.importXML(diagramXML).then(() => {}); +bpmnModeler.importXML(diagramXML).then(() => { + // Zoom up and center workflow in the middle of the canvas + bpmnModeler.get('canvas').zoom('fit-viewport', 'auto'); +}); // This handles the download and upload buttons - it isn't specific to // the BPMN modeler or these extensions, just a quick way to allow you to diff --git a/app/css/app.css b/app/css/app.css index 295da8b..44b5e80 100644 --- a/app/css/app.css +++ b/app/css/app.css @@ -1,6 +1,8 @@ - -html, body { +html, +body { height: 100%; + margin: 0; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; } .hidden { @@ -9,8 +11,7 @@ html, body { #container { display: flex; - width: 100%; - height: 100%; + height: calc(100% - 80px); } #modeler { @@ -18,16 +19,14 @@ html, body { } #panel { - background-color: #fafafa; - border: solid 1px #ccc; - border-radius: 2px; - font-family: 'Arial', sans-serif; - padding: 10px; - min-width: 400px; -} - -.djs-label { + background-color: white; + border-left: 1px solid #5F5F5F; + /* background-color: #fafafa; */ + /* border: solid 1px #ccc; */ + /* border-radius: 2px; */ font-family: 'Arial', sans-serif; + /* padding: 10px; */ + min-width: 350px; } .spiffworkflow-properties-panel-button { @@ -38,46 +37,307 @@ html, body { /* Style buttons */ .bpmn-js-spiffworkflow-btn { - background-color: DodgerBlue; - border: none; - color: white; + background-color: #ffffff; + color: #393939; + border: 1px solid #393939; padding: 8px 15px; cursor: pointer; font-size: 16px; margin: 12px; } +.main-btn { + background-color: #0F62FE; + color: white; + border: 1px solid #0F62FE; + padding: 8px 15px; + cursor: pointer; + font-size: 16px; + margin: 12px; +} + +.main-btn i { + margin-left: 15px; +} + /* Darker background on mouse-over */ .bpmn-js-spiffworkflow-btn:hover { - background-color: RoyalBlue; + background-color: rgb(0, 0, 0); + border: 1px solid #000000; + color: white; } /* Code Editor -- provided as a div overlay */ .overlay { - position: fixed; /* Sit on top of the page content */ - display: none; /* Hidden by default */ - width: 100%; /* Full width (cover the whole page) */ - height: 100%; /* Full height (cover the whole page) */ + position: fixed; + /* Sit on top of the page content */ + display: none; + /* Hidden by default */ + width: 100%; + /* Full width (cover the whole page) */ + height: 100%; + /* Full height (cover the whole page) */ top: 0; left: 0; right: 0; bottom: 0; - background-color: rgba(0,0,0,0.5); /* Black background with opacity */ - z-index: 200; /* BPMN Canvas has some huge z-indexes, pop-up tools are 100 for ex.*/ + background-color: rgba(0, 0, 0, 0.5); + /* Black background with opacity */ + z-index: 200; + /* BPMN Canvas has some huge z-indexes, pop-up tools are 100 for ex.*/ } -#code_editor, #markdown_editor { +#code_editor, +#markdown_editor { background-color: #ccc; margin: 50px auto 10px auto; max-width: 800px; } -#code_buttons, #markdown_buttons { +#code_buttons, +#markdown_buttons { margin: 50px auto 10px auto; max-width: 800px; right: 10px; } -.djs-palette.two-column.open { - width: 95px; +/* Header */ +#header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px; + background-color: #ffffff; + color: black; + border-bottom: 1px solid #5F5F5F; } + +#header-actions-center { + /* Adjust as needed */ + display: flex; + justify-content: center; + align-items: center; + border-radius: 8px; + border: 0.75px solid #8F8F8F; +} + +.header-btn { + background: none; + color: #3c3c3c; + border: none; + padding: 10px 10px; + cursor: pointer; + margin-left: 5px; + font-size: 20px; +} + +.header-btn i { + margin-right: 5px; + /* Icon spacing */ +} + + +#process-info h1 { + margin: 0; + font-size: 24px; +} + +#process-info p { + margin: 5px 0 0 0; + font-size: 14px; + color: #666; +} + +#header-actions { + display: flex; + align-items: center; +} + +.bpmn-js-spiffworkflow-btn { + margin-left: 8px; +} + +/* Left sidebar */ +#left-sidebar { + display: flex; + max-width: 300px; + background-color: #ffffff; + /* background-color: #f4f4f4; */ + overflow: hidden; + height: calc(100% - 80px); + float: left; +} + +.tabs { + display: flex; + flex-direction: column; + border-right: 1px solid #5F5F5F; +} + +.tab-button { + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + height: 50px; + background: none; + border: none; + cursor: pointer; + width: 100%; +} + +.tab-button i { + font-size: 18px; + color: #333; + /* margin-bottom: 5px; */ + padding: 5px; +} + +.tab-button .text { + font-size: 12px; + display: block; +} + +.tab-button.active i { + color: #146D83; +} + +.tab-button i { + transition: color 0.2s; +} + +.tab-button.active i { + transition: color 0.2s; +} + +.tab-button:hover { + background-color: #ddd; +} + +.tab-button.active, +.tab-button:hover { + background-color: #e9e9e9; + color: DodgerBlue; +} + +.tab-content { + display: none; + height: 100%; + width: 300px; + border-right: 1px solid #5F5F5F; +} + +.tab-content.active { + display: block; +} + +#container::after { + content: ""; + clear: both; + display: table; +} + +#BPMNElements { + border-bottom: 1px solid #5F5F5F; +} + +.bpmn-elements-header { + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid #5F5F5F; + padding: 10px; +} + +.bpmn-elements-title { + font-weight: bold; +} + +.bpmn-elements-toggle { + background: none; + border: none; + cursor: pointer; + padding: 5px; + font-size: 1em; +} + + +.group-title { + display: flex; + align-items: center; + justify-content: space-between; + padding-block: 5px; + padding-inline: 10px; + /* background: #f8f8f8; */ + /* background: #f5f5f58c; */ + /* border-bottom: 1px solid #ddd; */ + /* cursor: pointer; */ +} + +.group-title span { + font-weight: 600; +} + +.group.collapsed .entry { + display: none; +} + +.group-toggle { + background: none; + border: none; + cursor: pointer; +} + +.entry-label { + color: #22242A; + text-align: center; + font-family: unset; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: normal; +} + +.entries-container { + display: flex; + flex-wrap: wrap; + align-items: flex-start; + gap: 10px; +} + +.entry { + flex: 0 0 calc(25% - 10px); + display: flex; + align-items: center; + justify-content: center; + margin-block: 5px; +} + +.property-tabs { + display: -webkit-box; + width: 100%; + list-style-type: none; + padding: 0; + margin: 0; + border-right: unset; + justify-content: space-between; + text-align: center; +} + +.property-tabs li { + padding: 10px 20px; + cursor: pointer; + border-bottom: 2px solid grey; + + width: 50%; +} + +.property-tabs li.active { + border-bottom: 2px solid blue; + font-weight: bold; +} + +.tabs-group .tab-content { + width: 100%; + border: unset; +} \ No newline at end of file diff --git a/app/css/bpmn-js.css b/app/css/bpmn-js.css new file mode 100644 index 0000000..cbf179c --- /dev/null +++ b/app/css/bpmn-js.css @@ -0,0 +1,40 @@ + +/* Override default palette styles */ + +.djs-palette { + position: relative; + display: block; + width: 100% !important; + height: 90%; + overflow-y: auto; + padding: 0; + margin: 0; + left: 0; + top: 0; +} + +.djs-palette.two-column.open { + width: 95px; +} + +.djs-palette-entries { + display: grid; +} + +.djs-palette .entry, +.djs-palette .djs-palette-toggle { + justify-items: center; + height: unset; + width: unset; + line-height: unset; + display: inline-grid; + cursor: pointer; +} + +.bio-properties-panel-header { + background-color: #FAFAFA; +} + +.djs-label { + font-family: 'Arial', sans-serif; +} \ No newline at end of file diff --git a/app/favicon.png b/app/favicon.png new file mode 100644 index 0000000..1e572b2 Binary files /dev/null and b/app/favicon.png differ diff --git a/app/fileOperations.js b/app/fileOperations.js index 86a4a24..eb74d70 100644 --- a/app/fileOperations.js +++ b/app/fileOperations.js @@ -32,6 +32,28 @@ export default function setupFileOperations(bpmnModeler) { uploadBtn.addEventListener('click', (_event) => { openFile(bpmnModeler); }); + + // Handle header actions + const headerButtons = document.querySelectorAll('.header-btn'); + headerButtons.forEach(function (btn) { + btn.addEventListener('click', function (event) { + const action = event.target.closest('.header-btn').getAttribute('data-action'); + handleHeaderAction(action, bpmnModeler); + }); + }); + + // Handle sidebar toggle button + const toggleButtons = document.querySelectorAll('.bpmn-elements-toggle'); + toggleButtons.forEach(function (btn) { + btn.addEventListener('click', function (event) { + // Use a data attribute to identify which tab to toggle + const tabTarget = event.target.closest('button').getAttribute('data-tab-target'); + toggleTab(tabTarget); + }); + }); + + // Setup tabs after modeler is initialized + setupTabs(); } function clickElem(elem) { @@ -77,3 +99,88 @@ export function openFile(bpmnModeler) { document.body.appendChild(fileInput); clickElem(fileInput); } + +/** **************************************** + * Tab functionality + */ +function openTab(event, tabName) { + + // Hide all tab contents + const tabContents = document.querySelectorAll('.tab-content'); + tabContents.forEach(content => { + content.classList.remove('active'); + }); + + // Remove active class from all tabs + const tabButtons = document.querySelectorAll('.tab-button'); + tabButtons.forEach(button => { + button.classList.remove('active'); + }); + + // append active tab content class to the clicked tab button + document.getElementById(tabName).classList.add('active'); + event.currentTarget.classList.add('active'); +} + +function setupTabs() { + const tabs = document.querySelectorAll('.tab-button'); + tabs.forEach(tab => { + tab.addEventListener('click', function (event) { + openTab(event, this.getAttribute('data-tab-target')); + }); + }); +} + +function toggleTab(tabId) { + const tabContent = document.getElementById(tabId); + const allTabContents = document.querySelectorAll('.tab-content'); + + // Remove 'active' from all tabs + allTabContents.forEach(function (tab) { + tab.classList.remove('active'); + }); +} + +/** + * Header functionality + */ +function handleHeaderAction(action, bpmnModeler) { + var commandStack = bpmnModeler.get('commandStack'); + var paletteProvider = bpmnModeler.get('paletteProvider'); + var canvas = bpmnModeler.get('canvas'); + switch (action) { + case 'zoom-in': + bpmnModeler.get('zoomScroll').stepZoom(1); + break; + case 'zoom-out': + bpmnModeler.get('zoomScroll').stepZoom(-1); + break; + case 'expand': + canvas.zoom('fit-viewport', 'auto'); + break; + case 'undo': + commandStack.undo(); + break; + case 'redo': + commandStack.redo(); + break; + case 'hand': + const handTool = paletteProvider._handTool; + handTool.activateHand(); + break; + case 'lasso': + const lassoTool = paletteProvider._lassoTool; + lassoTool.activateSelection(event); + break; + case 'space': + const spaceTool = paletteProvider._spaceTool; + spaceTool.activateSelection(); + break; + case 'connect': + const globalConnect = paletteProvider._globalConnect; + globalConnect.start(); + break; + default: + console.log('Unknown action:', action); + } +} \ No newline at end of file diff --git a/app/index.html b/app/index.html index ca7d9aa..0e9f839 100644 --- a/app/index.html +++ b/app/index.html @@ -1,22 +1,19 @@ - - - bpmn-js-spiffworkflow - + + bpmn-js-spiffworkflow + + - - - - + + + + - + + @@ -32,33 +29,145 @@ - - -
-
-
-
- -
-
-
- -
-
-
-
- -
-
- -
-
- - + + + +
+
+
+
+ +
+
+
+ +
+
+
+
+ +
+
+ +
+
+ + + - + + \ No newline at end of file diff --git a/app/spiffworkflow/DataObject/propertiesPanel/DataObjectPropertiesProvider.js b/app/spiffworkflow/DataObject/propertiesPanel/DataObjectPropertiesProvider.js index 79afcf6..a62ae90 100644 --- a/app/spiffworkflow/DataObject/propertiesPanel/DataObjectPropertiesProvider.js +++ b/app/spiffworkflow/DataObject/propertiesPanel/DataObjectPropertiesProvider.js @@ -68,6 +68,7 @@ function createDataObjectSelector(element, translate, moddle, commandStack, mode return { id: 'data_object_properties', label: translate('Data Object Properties'), + isDefault: true, entries: [ { id: 'selectDataObject', diff --git a/app/spiffworkflow/DataStoreReference/propertiesPanel/DataStorePropertiesProvider.js b/app/spiffworkflow/DataStoreReference/propertiesPanel/DataStorePropertiesProvider.js index ce227d1..acca651 100644 --- a/app/spiffworkflow/DataStoreReference/propertiesPanel/DataStorePropertiesProvider.js +++ b/app/spiffworkflow/DataStoreReference/propertiesPanel/DataStorePropertiesProvider.js @@ -51,6 +51,7 @@ function createCustomDataStoreGroup( const group = { label: translate('Custom Data Store Properties'), id: 'custom-datastore-properties', + isDefault: true, entries: [], }; diff --git a/app/spiffworkflow/InputOutput/IoPalette.js b/app/spiffworkflow/InputOutput/IoPalette.js index e8f9567..2eb1da5 100644 --- a/app/spiffworkflow/InputOutput/IoPalette.js +++ b/app/spiffworkflow/InputOutput/IoPalette.js @@ -4,19 +4,34 @@ import translate from 'diagram-js/lib/i18n/translate/translate'; /** * Add data inputs and data outputs to the panel. */ -export default function IoPalette(palette, create, elementFactory,) { +export default function IoPalette(palette, create, elementFactory, eventBus, handTool, globalConnect, lassoTool, spaceTool) { this._create = create; this._elementFactory = elementFactory; + + this._handTool = handTool; + this._globalConnect = globalConnect; + this._lassoTool = lassoTool; + this._spaceTool = spaceTool; + + eventBus.on('palette.create', function (event) { + this.init(event); + }.bind(this)); + palette.registerProvider(this); } IoPalette.$inject = [ 'palette', 'create', - 'elementFactory' + 'elementFactory', + 'eventBus', + 'handTool', + 'globalConnect', + 'lassoTool', + 'spaceTool' ]; -IoPalette.prototype.getPaletteEntries = function() { +IoPalette.prototype.getPaletteEntries = function (e) { let input_type = 'bpmn:DataInput'; let output_type = 'bpmn:DataOutput'; @@ -24,7 +39,7 @@ IoPalette.prototype.getPaletteEntries = function() { function createListener(event, type) { let shape = elementFactory.createShape(assign({ type: type }, {})); - shape.width = 36; // Fix up the shape dimensions from the defaults. + shape.width = 36; shape.height = 50; create.start(event, shape); } @@ -37,26 +52,268 @@ IoPalette.prototype.getPaletteEntries = function() { createListener(event, output_type); } + function createShape(type, options = {}) { + return function (event) { + let shape = elementFactory.createShape(assign({ type: type }, options)); + create.start(event, shape); + }; + } + return { + // Events + 'create.start-event': { + group: 'events', + className: 'bpmn-icon-start-event-none', + title: translate('Start'), + action: { + dragstart: createShape('bpmn:StartEvent'), + click: createShape('bpmn:StartEvent') + } + }, + 'create.intermediate-event': { + group: 'events', + className: 'bpmn-icon-intermediate-event-none', + title: translate('Intermediate'), + action: { + dragstart: createShape('bpmn:IntermediateCatchEvent'), + click: createShape('bpmn:IntermediateCatchEvent') + } + }, + 'create.end-event': { + group: 'events', + className: 'bpmn-icon-end-event-none', + title: translate('End'), + action: { + dragstart: createShape('bpmn:EndEvent'), + click: createShape('bpmn:EndEvent') + } + }, + // Activities + 'create.task': { + group: 'activities', + className: 'bpmn-icon-task', + title: translate('Task'), + action: { + dragstart: createShape('bpmn:Task'), + click: createShape('bpmn:Task') + } + }, + 'create.user-task': { + group: 'activities', + className: 'bpmn-icon-user', + title: translate('User Task'), + action: { + dragstart: createShape('bpmn:UserTask'), + click: createShape('bpmn:UserTask') + } + }, + 'create.scirpt-task': { + group: 'activities', + className: 'bpmn-icon-script', + title: translate('Script Task'), + action: { + dragstart: createShape('bpmn:ScriptTask'), + click: createShape('bpmn:ScriptTask') + } + }, + 'create.service-task': { + group: 'activities', + className: 'bpmn-icon-service', + title: translate('Service Task'), + action: { + dragstart: createShape('bpmn:ServiceTask'), + click: createShape('bpmn:ServiceTask') + } + }, + // 'create.dmn-task': { + // group: 'activities', + // className: 'bpmn-icon-business-rule', + // title: translate('Business Rule Task'), + // action: { + // dragstart: createShape('bpmn:BusinessRuleTask'), + // click: createShape('bpmn:BusinessRuleTask') + // } + // }, + // Gateways + 'create.condition-gateaway': { + group: 'decisions', + className: 'bpmn-icon-gateway-xor', + title: translate('Decision'), + action: { + dragstart: createShape('bpmn:ExclusiveGateway'), + click: createShape('bpmn:ExclusiveGateway') + } + }, + 'create.parallel-gateaway': { + group: 'decisions', + className: 'bpmn-icon-gateway-parallel', + title: translate('Parallel'), + action: { + dragstart: createShape('bpmn:ParallelGateway'), + click: createShape('bpmn:ParallelGateway') + } + }, + 'create.eventbased-gateaway': { + group: 'decisions', + className: 'bpmn-icon-gateway-eventbased', + title: translate('Event Based'), + action: { + dragstart: createShape('bpmn:EventBasedGateway'), + click: createShape('bpmn:EventBasedGateway') + } + }, + 'create.inclusive-gateaway': { + group: 'decisions', + className: 'bpmn-icon-gateway-or', + title: translate('xOR'), + action: { + dragstart: createShape('bpmn:InclusiveGateway'), + click: createShape('bpmn:InclusiveGateway') + } + }, + // Data Object + 'create.data-store': { + group: 'data', + className: 'bpmn-icon-data-store', + title: translate('Data Store'), + action: { + dragstart: createShape('bpmn:DataStoreReference'), + click: createShape('bpmn:DataStoreReference') + } + }, + 'create.data-object': { + group: 'data', + className: 'bpmn-icon-data-object', + title: translate('Data Object'), + action: { + dragstart: createShape('bpmn:DataObjectReference'), + click: createShape('bpmn:DataObjectReference') + } + }, 'create.data-input': { - group: 'data-object', + group: 'data', className: 'bpmn-icon-data-input', - title: translate('Create DataInput'), + title: translate('Data Input'), action: { dragstart: createInputListener, click: createInputListener } }, 'create.data-output': { - group: 'data-object', + group: 'data', className: 'bpmn-icon-data-output', - title: translate('Create DataOutput'), + title: translate('Data Output'), action: { dragstart: createOutputListener, click: createOutputListener } - } - + }, + // Advanced + 'create.call-activity': { + group: 'advanced', + className: 'bpmn-icon-call-activity', + title: translate('Call Activity'), + action: { + dragstart: createShape('bpmn:CallActivity'), + click: createShape('bpmn:CallActivity') + } + }, + 'create.participant': { + group: 'advanced', + className: 'bpmn-icon-participant', + title: translate('Participant'), + action: { + dragstart: createShape('bpmn:Participant'), + click: createShape('bpmn:Participant') + } + }, + 'create.sub-process-expanded': { + group: 'advanced', + className: 'bpmn-icon-subprocess-expanded', + title: translate('Sub Process'), + action: { + dragstart: createShape('bpmn:SubProcess', { isExpanded: true }), + click: createShape('bpmn:SubProcess', { isExpanded: true }) + } + }, + 'create.transaction': { + group: 'advanced', + className: 'bpmn-icon-transaction', + title: translate('Transaction'), + action: { + dragstart: createShape('bpmn:Transaction', { isExpanded: true }), + click: createShape('bpmn:Transaction', { isExpanded: true }) + } + }, }; }; +IoPalette.prototype.init = function (event) { + + // Override Palette DOM Generated by BPMN-JS Library + const paletteContainer = event.container; + const bpmnElementsDiv = document.getElementById('BPMNElements'); + + setTimeout(() => { + + // Query all group elements + const groups = paletteContainer.querySelectorAll('.group'); + + groups.forEach(group => { + const groupName = group.getAttribute('data-group'); + const title = groupName.charAt(0).toUpperCase() + groupName.slice(1).replace(/-/g, ' '); // Capitalize and format the title + + // Check if group title already exists + let header = group.querySelector('.group-title'); + let entriesContainer = group.querySelector('.entries-container'); // Container for entries + + if (!header) { + // Creation the collapsible header + header = document.createElement('div'); + header.classList.add('group-title'); + + // Creation the title span + const titleSpan = document.createElement('span'); + titleSpan.textContent = title; + header.appendChild(titleSpan); + + // Creation the toggle button + const toggleButton = document.createElement('button'); + toggleButton.classList.add('group-toggle'); + toggleButton.innerHTML = ''; + header.appendChild(toggleButton); + + // Insert the header + group.insertBefore(header, group.firstChild); + + // Create the entries container + entriesContainer = document.createElement('div'); + entriesContainer.classList.add('entries-container'); + group.appendChild(entriesContainer); // Append entries container after the header + + toggleButton.addEventListener('click', function () { + entriesContainer.style.display = entriesContainer.style.display === 'none' ? '' : 'none'; + toggleButton.innerHTML = entriesContainer.style.display === 'none' ? '' : ''; + }); + } + + const entries = group.querySelectorAll('.entry'); + entries.forEach(entry => { + entriesContainer.appendChild(entry); + + let label = entry.querySelector('.entry-label'); + if (!label) { + label = document.createElement('span'); + label.classList.add('entry-label'); + entry.appendChild(label); + } + label.textContent = entry.getAttribute('title'); + }); + }); + + // Move the palette + bpmnElementsDiv.appendChild(paletteContainer); + + }, 0); + +}; \ No newline at end of file diff --git a/app/spiffworkflow/callActivity/propertiesPanel/CallActivityPropertiesProvider.js b/app/spiffworkflow/callActivity/propertiesPanel/CallActivityPropertiesProvider.js index 8aee91d..732e5a1 100644 --- a/app/spiffworkflow/callActivity/propertiesPanel/CallActivityPropertiesProvider.js +++ b/app/spiffworkflow/callActivity/propertiesPanel/CallActivityPropertiesProvider.js @@ -36,6 +36,7 @@ function createCalledElementGroup(element, translate, moddle, commandStack) { return { id: 'called_element', label: translate('Called Element'), + isDefault: true, entries: [ { id: `called_element_text_field`, diff --git a/app/spiffworkflow/conditions/propertiesPanel/ConditionsPropertiesProvider.js b/app/spiffworkflow/conditions/propertiesPanel/ConditionsPropertiesProvider.js index 05de8ff..8daaa74 100644 --- a/app/spiffworkflow/conditions/propertiesPanel/ConditionsPropertiesProvider.js +++ b/app/spiffworkflow/conditions/propertiesPanel/ConditionsPropertiesProvider.js @@ -48,6 +48,8 @@ function createConditionsGroup(element, translate, moddle, commandStack) { return { id: 'conditions', label: translate('Conditions'), + // is default property is mainly used to mark this group as a high priority group that located in General Tab + isDefault: true, entries: conditionGroup( element, moddle, diff --git a/app/spiffworkflow/eventSelect.js b/app/spiffworkflow/eventSelect.js index ffd823f..4f906d7 100644 --- a/app/spiffworkflow/eventSelect.js +++ b/app/spiffworkflow/eventSelect.js @@ -87,6 +87,7 @@ function getConfigureGroupForType(eventDetails, label, includeCode, getSelect) { return { id: `${idPrefix}-group`, label: label, + isDefault: true, entries, } } diff --git a/app/spiffworkflow/extensions/extensionHelpers.js b/app/spiffworkflow/extensions/extensionHelpers.js index c0059f7..0d9c874 100644 --- a/app/spiffworkflow/extensions/extensionHelpers.js +++ b/app/spiffworkflow/extensions/extensionHelpers.js @@ -95,7 +95,7 @@ export function setExtensionValue(element, name, value, moddle, commandStack, bu } function getExtension(businessObject, name) { - if (!businessObject || !businessObject.extensionElements) { + if (!businessObject.extensionElements) { return null; } const extensionElements = businessObject.extensionElements.get('values'); diff --git a/app/spiffworkflow/extensions/propertiesPanel/ExtensionsPropertiesProvider.js b/app/spiffworkflow/extensions/propertiesPanel/ExtensionsPropertiesProvider.js index d1d7a11..6abe53a 100644 --- a/app/spiffworkflow/extensions/propertiesPanel/ExtensionsPropertiesProvider.js +++ b/app/spiffworkflow/extensions/propertiesPanel/ExtensionsPropertiesProvider.js @@ -9,14 +9,14 @@ import { ServiceTaskParameterArray, ServiceTaskOperatorSelect, ServiceTaskResultTextInput, } from './SpiffExtensionServiceProperties'; -import {OPTION_TYPE, spiffExtensionOptions, SpiffExtensionSelect} from './SpiffExtensionSelect'; -import {SpiffExtensionLaunchButton} from './SpiffExtensionLaunchButton'; -import {SpiffExtensionTextArea} from './SpiffExtensionTextArea'; -import {SpiffExtensionTextInput} from './SpiffExtensionTextInput'; -import {SpiffExtensionCheckboxEntry} from './SpiffExtensionCheckboxEntry'; -import {hasEventDefinition} from 'bpmn-js/lib/util/DiUtil'; +import { OPTION_TYPE, spiffExtensionOptions, SpiffExtensionSelect } from './SpiffExtensionSelect'; +import { SpiffExtensionLaunchButton } from './SpiffExtensionLaunchButton'; +import { SpiffExtensionTextArea } from './SpiffExtensionTextArea'; +import { SpiffExtensionTextInput } from './SpiffExtensionTextInput'; +import { SpiffExtensionCheckboxEntry } from './SpiffExtensionCheckboxEntry'; +import { hasEventDefinition } from 'bpmn-js/lib/util/DiUtil'; import { PropertyDescription } from 'bpmn-js-properties-panel/'; -import {setExtensionValue} from "../extensionHelpers"; +import { setExtensionValue } from "../extensionHelpers"; const LOW_PRIORITY = 500; @@ -112,6 +112,8 @@ function createScriptGroup(element, translate, moddle, commandStack) { return { id: 'spiff_script', label: translate('Script'), + // is default property is mainly used to mark this group as a high priority group that located in General Tab + isDefault: true, entries: scriptGroup({ element, moddle, @@ -154,7 +156,7 @@ function preScriptPostScriptGroup(element, translate, moddle, commandStack) { }), ]; const loopCharacteristics = element.businessObject.loopCharacteristics; - if (typeof(loopCharacteristics) !== 'undefined') { + if (typeof (loopCharacteristics) !== 'undefined') { entries.push({ id: 'scriptValence', component: ScriptValenceCheckbox, @@ -211,13 +213,14 @@ function createUserGroup(element, translate, moddle, commandStack) { setExtensionValue(element, 'formUiSchemaFilename', uiName, moddle, commandStack); const matches = spiffExtensionOptions[OPTION_TYPE.json_schema_files].filter((opt) => opt.value === value); if (matches.length === 0) { - spiffExtensionOptions[OPTION_TYPE.json_schema_files].push({label: value, value: value}); + spiffExtensionOptions[OPTION_TYPE.json_schema_files].push({ label: value, value: value }); } } return { id: 'user_task_properties', label: translate('Web Form (with Json Schemas)'), + isDefault: true, entries: [ { element, @@ -288,7 +291,7 @@ function createBusinessRuleGroup(element, translate, moddle, commandStack) { * @param moddle * @returns entries */ -function createUserInstructionsGroup ( +function createUserInstructionsGroup( element, translate, moddle, @@ -329,7 +332,7 @@ function createUserInstructionsGroup ( * @param moddle * @returns entries */ -function createAllowGuestGroup ( +function createAllowGuestGroup( element, translate, moddle, @@ -379,14 +382,14 @@ function createAllowGuestGroup ( * @param moddle * @returns entries */ -function createSignalButtonGroup ( +function createSignalButtonGroup( element, translate, moddle, commandStack ) { let description = -

If attached to a user/manual task, setting this value will display a button which a user can click to immediately fire this signal event. +

If attached to a user/manual task, setting this value will display a button which a user can click to immediately fire this signal event.

return { id: 'signal_button', @@ -417,6 +420,7 @@ function createServiceGroup(element, translate, moddle, commandStack) { return { id: 'service_task_properties', label: translate('Spiffworkflow Service Properties'), + isDefault: true, entries: [ { element, diff --git a/app/spiffworkflow/index.js b/app/spiffworkflow/index.js index 7cfa5a2..7650d8d 100644 --- a/app/spiffworkflow/index.js +++ b/app/spiffworkflow/index.js @@ -18,6 +18,7 @@ import EscalationPropertiesProvider from './escalations/propertiesPanel/Escalati import CallActivityPropertiesProvider from './callActivity/propertiesPanel/CallActivityPropertiesProvider'; import StandardLoopPropertiesProvider from './loops/propertiesPanel/StandardLoopPropertiesProvider'; import MultiInstancePropertiesProvider from './loops/propertiesPanel/MultiInstancePropertiesProvider'; +import PropertiesPanelProvider from './properties/PropertiesPanelProvider'; export default { __depends__: [RulesModule], @@ -36,6 +37,8 @@ export default { 'escalationPropertiesProvider', 'callActivityPropertiesProvider', 'ioPalette', + 'paletteProvider', + 'propertiesPanelProvider', 'ioRules', 'ioInterceptor', 'dataObjectRenderer', @@ -57,6 +60,8 @@ export default { messagesPropertiesProvider: ['type', MessagesPropertiesProvider], callActivityPropertiesProvider: ['type', CallActivityPropertiesProvider], ioPalette: ['type', IoPalette], + paletteProvider: ['type', IoPalette], + propertiesPanelProvider: ['type', PropertiesPanelProvider], ioRules: ['type', IoRules], ioInterceptor: ['type', IoInterceptor], multiInstancePropertiesProvider: ['type', MultiInstancePropertiesProvider], diff --git a/app/spiffworkflow/messages/propertiesPanel/MessagesPropertiesProvider.js b/app/spiffworkflow/messages/propertiesPanel/MessagesPropertiesProvider.js index 6abf6c7..41dfee7 100644 --- a/app/spiffworkflow/messages/propertiesPanel/MessagesPropertiesProvider.js +++ b/app/spiffworkflow/messages/propertiesPanel/MessagesPropertiesProvider.js @@ -181,6 +181,7 @@ function createMessageGroup( return { id: 'messages', label: translate('Message'), + isDefault: true, entries, }; } diff --git a/app/spiffworkflow/properties/PropertiesPanelProvider.js b/app/spiffworkflow/properties/PropertiesPanelProvider.js new file mode 100644 index 0000000..ad1b8c1 --- /dev/null +++ b/app/spiffworkflow/properties/PropertiesPanelProvider.js @@ -0,0 +1,148 @@ +const LOW_PRIORITY = 800; + +export default function PropertiesPanelProvider(propertiesPanel, eventBus) { + let elId; + + // eventBus.on('propertiesPanel.providersChanged', function (event) { + // console.log('------------------- propertiesPanel.providersChanged', event); + // }); + + // eventBus.on('propertiesPanel.getProviders', function (event) { + // console.log('------------------- propertiesPanel.getProviders', event); + // }); + + // eventBus.on('propertiesPanel.setLayout', function (event) { + // console.log('------------------- propertiesPanel.setLayout', event); + // }); + + // eventBus.on('propertiesPanel.layoutChanged', function (event) { + // console.log('------------------- propertiesPanel.layoutChanged', event); + // }); + + this.getGroups = function (element) { + return function (groups) { + // console.log('PropertiesPanelProvider -> getGroups: ', groups, propertiesPanel, element); + // Only render the properties panel once per element + if (element.id !== elId || !elId) { + elId = element.id; + this.render(groups); + } + return groups; + }.bind(this); + }; + + propertiesPanel.registerProvider(LOW_PRIORITY, this); +} + +PropertiesPanelProvider.$inject = ['propertiesPanel', 'eventBus']; + +PropertiesPanelProvider.prototype.render = function (groups) { + + setTimeout(() => { + const propertiesPanelContainer = document.querySelector('.bio-properties-panel-container'); + if (!propertiesPanelContainer) return; + + // Within that big container, find the part where we can scroll + const scrollContainer = propertiesPanelContainer.querySelector('.bio-properties-panel-scroll-container'); + if (!scrollContainer) return; + + // This function makes the groups able to open and close. + function makeGroupCollapsible(group) { + const header = group.querySelector('.bio-properties-panel-group-header'); + const entries = group.querySelector('.bio-properties-panel-group-entries'); + + if (header && entries) { + header.classList.add('open'); + entries.classList.add('open'); + + // When you click the header, it should open or close + header.addEventListener('click', function() { + header.classList.toggle('open'); + entries.classList.toggle('open'); + }); + } + } + + // This function decides what to show based on which tab is clicked. + function updateTabContent(activeTab) { + const allGroups = scrollContainer.querySelectorAll('.bio-properties-panel-group'); + allGroups.forEach(group => group.style.display = 'none'); // Hide everything first. + + groups.forEach(group => { + const groupElement = scrollContainer.querySelector(`[data-group-id="group-${group.id}"]`); + if (groupElement) { + // If we're on the "General" tab, show the general groups and any group that's set as default. + if (activeTab.dataset.tab === 'general' && (group.id === 'general' || group.isDefault)) { + groupElement.style.display = ''; + if (group.isDefault) { + makeGroupCollapsible(groupElement); + } + } else if (activeTab.dataset.tab === 'advanced' && group.id !== 'general' && !group.isDefault) { + groupElement.style.display = ''; + } + } + }); + } + + // Add a click event to each tab to change what's shown + document.querySelectorAll('.tabs li').forEach(tab => { + tab.addEventListener('click', function(event) { + document.querySelectorAll('.tabs li').forEach(t => t.classList.remove('active')); + tab.classList.add('active'); + updateTabContent(tab); + }); + }); + + // Function to always start with the 'General' tab opened + function resetTabsToShowGeneral() { + const generalTab = scrollContainer.querySelector('li[data-tab="general"]'); + const advancedTab = scrollContainer.querySelector('li[data-tab="advanced"]'); + + if (generalTab && advancedTab) { + generalTab.classList.add('active'); + advancedTab.classList.remove('active'); + updateTabContent(generalTab); + } + } + + // Create the tabs if they don't exist yet. + if (!scrollContainer.querySelector('.tabs')) { + const tabsHeader = document.createElement('ul'); + tabsHeader.className = 'tabs property-tabs'; + + const generalTab = document.createElement('li'); + generalTab.textContent = 'General'; + generalTab.dataset.tab = 'general'; + generalTab.className = 'active'; + tabsHeader.appendChild(generalTab); + + const advancedTab = document.createElement('li'); + advancedTab.textContent = 'Advanced'; + advancedTab.dataset.tab = 'advanced'; + tabsHeader.appendChild(advancedTab); + + scrollContainer.insertBefore(tabsHeader, scrollContainer.firstChild); + } + + // Make sure each tab can do its thing when clicked. + const tabs = scrollContainer.querySelectorAll('.tabs li'); + tabs.forEach(tab => { + if (!tab.dataset.listenerAttached) { + tab.addEventListener('click', function(event) { + tabs.forEach(t => t.classList.remove('active')); + tab.classList.add('active'); + updateTabContent(tab); + }); + tab.dataset.listenerAttached = 'true'; // This is just to make sure we don't add the same event more than once. + } + }); + + // When we first load, show the right tab and its contents. + const activeTab = document.querySelector('.tabs li.active'); + if (activeTab) { + updateTabContent(activeTab); + } + + resetTabsToShowGeneral(); + }, 0); +} diff --git a/test/spec/BpmnInputOutputSpec.js b/test/spec/BpmnInputOutputSpec.js index 0d2c201..9e7286b 100644 --- a/test/spec/BpmnInputOutputSpec.js +++ b/test/spec/BpmnInputOutputSpec.js @@ -22,8 +22,8 @@ describe('BPMN Input / Output', function() { it('should have a data input and data output in the properties panel', function() { var paletteElement = domQuery('.djs-palette', CONTAINER); var entries = domQueryAll('.entry', paletteElement); - expect(entries[11].title).to.equals('Create DataInput'); - expect(entries[12].title).to.equals('Create DataOutput'); + expect(entries[14].title).to.equals('Data Input'); + expect(entries[15].title).to.equals('Data Output'); }); });