diff --git a/administrator/components/com_workflow/layouts/toolbar/redo.php b/administrator/components/com_workflow/layouts/toolbar/redo.php index 08355f03db0..fb63baf3f21 100644 --- a/administrator/components/com_workflow/layouts/toolbar/redo.php +++ b/administrator/components/com_workflow/layouts/toolbar/redo.php @@ -13,13 +13,19 @@ use Joomla\CMS\Factory; use Joomla\CMS\Language\Text; -Factory::getDocument()->getWebAssetManager() +Factory::getApplication()->getDocument()->getWebAssetManager() ->useScript('webcomponent.toolbar-button'); ?> - + + + diff --git a/administrator/components/com_workflow/layouts/toolbar/shortcuts.php b/administrator/components/com_workflow/layouts/toolbar/shortcuts.php index 165f7f64633..41788338ccd 100644 --- a/administrator/components/com_workflow/layouts/toolbar/shortcuts.php +++ b/administrator/components/com_workflow/layouts/toolbar/shortcuts.php @@ -13,7 +13,7 @@ use Joomla\CMS\Factory; use Joomla\CMS\Language\Text; -Factory::getDocument()->getWebAssetManager() +Factory::getApplication()->getDocument()->getWebAssetManager() ->useScript('webcomponent.toolbar-button'); $shortcutsPopupOptions = json_encode([ diff --git a/administrator/components/com_workflow/layouts/toolbar/undo.php b/administrator/components/com_workflow/layouts/toolbar/undo.php index f42002b9e94..2e16cfc5901 100644 --- a/administrator/components/com_workflow/layouts/toolbar/undo.php +++ b/administrator/components/com_workflow/layouts/toolbar/undo.php @@ -13,13 +13,21 @@ use Joomla\CMS\Factory; use Joomla\CMS\Language\Text; -Factory::getDocument()->getWebAssetManager() +Factory::getApplication()->getDocument()->getWebAssetManager() ->useScript('webcomponent.toolbar-button'); ?> - + + + + + diff --git a/administrator/components/com_workflow/resources/scripts/components/App.vue b/administrator/components/com_workflow/resources/scripts/components/App.vue index 692a1f37f81..1a032b432a5 100644 --- a/administrator/components/com_workflow/resources/scripts/components/App.vue +++ b/administrator/components/com_workflow/resources/scripts/components/App.vue @@ -1,13 +1,11 @@ { const { workflowId: idFromOpts = null } = Joomla.getOptions('com_workflow', {}); const idFromURL = parseInt(new URL(window.location.href).searchParams.get('id'), 10); - const workflowIdFinal = idFromOpts || idFromURL; + const currentWorkflowId = idFromOpts || idFromURL; - if (workflowIdFinal !== null && !Number.isNaN(workflowIdFinal)) { - store.dispatch('loadWorkflow', workflowIdFinal); + if (currentWorkflowId !== null && !Number.isNaN(currentWorkflowId)) { + store.dispatch('loadWorkflow', currentWorkflowId); } else { throw new Error('COM_WORKFLOW_GRAPH_ERROR_INVALID_ID'); } diff --git a/administrator/components/com_workflow/resources/scripts/components/Titlebar.vue b/administrator/components/com_workflow/resources/scripts/components/Titlebar.vue index 66175209893..a9fc85b16e5 100644 --- a/administrator/components/com_workflow/resources/scripts/components/Titlebar.vue +++ b/administrator/components/com_workflow/resources/scripts/components/Titlebar.vue @@ -1,60 +1,51 @@ - + {{ translate(workflow?.title) }} - - - {{ translate('COM_WORKFLOW_GRAPH_STATUS') }} - - - - {{ workflow.published ? translate('COM_WORKFLOW_GRAPH_ENABLED') : translate('COM_WORKFLOW_GRAPH_DISABLED') }} - - - - - - {{ translate('COM_WORKFLOW_GRAPH_STAGE_COUNT') }} - - - {{ stagesCount }} {{ stagesCount === 1 ? translate('COM_WORKFLOW_GRAPH_STAGE') : translate('COM_WORKFLOW_GRAPH_STAGES') }} - - - - - {{ translate('COM_WORKFLOW_GRAPH_TRANSITION_COUNT') }} - - - {{ transitionsCount }} {{ transitionsCount === 1 ? translate('COM_WORKFLOW_GRAPH_TRANSITION') : - translate('COM_WORKFLOW_GRAPH_TRANSITIONS') }} - - + + {{ sprintf('COM_WORKFLOW_GRAPH_STATUS', workflow.published ? 'COM_WORKFLOW_GRAPH_ENABLED' : 'COM_WORKFLOW_GRAPH_DISABLED') }} + + + + {{ workflow.published ? translate('COM_WORKFLOW_GRAPH_ENABLED') : translate('COM_WORKFLOW_GRAPH_DISABLED') }} + + + + + {{ sprintf('COM_WORKFLOW_GRAPH_STAGE_COUNT', stagesCount) }} + + + {{ stagesCount }} {{ stagesCount === 1 ? translate('COM_WORKFLOW_GRAPH_STAGE') : translate('COM_WORKFLOW_GRAPH_STAGES') }} + + + + {{ sprintf('COM_WORKFLOW_GRAPH_TRANSITION_COUNT', transitionsCount) }} + + + {{ transitionsCount }} {{ transitionsCount === 1 ? translate('COM_WORKFLOW_GRAPH_TRANSITION') + : translate('COM_WORKFLOW_GRAPH_TRANSITIONS') }} + {{ saveStatus.value === 'unsaved' diff --git a/administrator/components/com_workflow/resources/scripts/components/canvas/ControlsPanel.vue b/administrator/components/com_workflow/resources/scripts/components/canvas/ControlsPanel.vue index d4c5410ae12..c8d1b1e5618 100644 --- a/administrator/components/com_workflow/resources/scripts/components/canvas/ControlsPanel.vue +++ b/administrator/components/com_workflow/resources/scripts/components/canvas/ControlsPanel.vue @@ -29,21 +29,6 @@ /> {{ translate('COM_WORKFLOW_GRAPH_ADD_TRANSITION') }} - - @@ -53,12 +38,6 @@ import { Panel } from '@vue-flow/core'; export default { name: 'ControlsPanel', components: { Panel }, - props: { - isTransitionMode: { - type: Boolean, - default: false, - }, - }, - emits: ['add-stage', 'add-transition', 'toggle-transition-mode'], + emits: ['add-stage', 'add-transition'], }; diff --git a/administrator/components/com_workflow/resources/scripts/components/canvas/CustomControls.vue b/administrator/components/com_workflow/resources/scripts/components/canvas/CustomControls.vue index 72119966a7e..f759e0494c0 100644 --- a/administrator/components/com_workflow/resources/scripts/components/canvas/CustomControls.vue +++ b/administrator/components/com_workflow/resources/scripts/components/canvas/CustomControls.vue @@ -1,7 +1,7 @@ - + { const nodes = generatePositionedNodes(stages.value); - const special = createSpecialNode('from_any', { x: 600, y: -200 }, '#EF4444', 'From Any', selectStage, isTransitionMode.value, true); + const special = createSpecialNode('from_any', { x: 600, y: -200 }, '#ffff00', 'From Any', selectStage, false); return [...nodes.map((n) => ({ ...n, data: { ...n.data, isSelected: selectedStage.value === parseInt(n.id, 10), onSelect: () => selectStage(n.id), + onEscape: () => clearSelection(), onEdit: () => editStage(n.id), onDelete: () => showDeleteModal('stage', n.id), }, - // draggable: !isTransitionMode.value, })), special]; }); const styledEdges = computed(() => generateStyledEdges(transitions.value, { - transitionMode: isTransitionMode.value, selectedId: selectedTransition.value, }).map((edge) => ({ ...edge, data: { ...edge.data, onSelect: () => selectTransition(edge.id), + onEscape: () => clearSelection(), onDelete: () => showDeleteModal('transition', edge.id), onEdit: () => editTransition(edge.id), }, @@ -375,7 +368,6 @@ export default { if (selectedStage.value) showDeleteModal('stage', selectedStage.value); else if (selectedTransition.value) showDeleteModal('transition', selectedTransition.value); }, - toggleMode: toggleTransitionMode, undo: () => { if (!store.getters.canUndo) { return; @@ -434,15 +426,6 @@ export default { saveNodePosition(); }); - // window.WorkflowGraph.Event.fire('Error', { error: error.message }); - window.WorkflowGraph.Event.listen('Error', (event) => { - if (window.Joomla && window.Joomla.renderMessages) { - window.Joomla.renderMessages({ - error: [event.error.message], - }); - } - }); - let isRestoringViewport = false; watch([loading, error], () => { setTimeout(() => { @@ -485,13 +468,11 @@ export default { positionedNodes, styledEdges, liveRegion, - isTransitionMode, handleConnect, selectEdge, handleDeleteConfirm, addStage, addTransition, - toggleTransitionMode, clearSelection, handleNodeDragStop, }; diff --git a/administrator/components/com_workflow/resources/scripts/components/edges/CustomEdge.vue b/administrator/components/com_workflow/resources/scripts/components/edges/CustomEdge.vue index 11e3c0a4346..8b336ade898 100644 --- a/administrator/components/com_workflow/resources/scripts/components/edges/CustomEdge.vue +++ b/administrator/components/com_workflow/resources/scripts/components/edges/CustomEdge.vue @@ -9,60 +9,136 @@ :stroke-dasharray="style?.strokeDasharray" :marker-end="markerEnd" /> - - + + + + + + {{ data?.title }} + + {{ translate('COM_WORKFLOW_GRAPH_EDIT_TRANSITION') }} + {{ translate('COM_WORKFLOW_GRAPH_DELETE_TRANSITION_TITLE') }} - + + + {{ data?.title }} + + + + + + + + + + + {{ data?.title }} + + @@ -113,17 +189,28 @@ export default { default: () => ({}), }, }, + data() { + return { + showActions: false, + isHovered: false, + maxHeight: 100, + maxWidth: 100, + currentMenuIndex: -1, + blurTimeout: null, + hoverTimeout: null, + }; + }, computed: { edgeData() { return getSmoothStepPath({ sourceX: this.sourceX, sourceY: this.sourceY, - sourcePosition: this.sourcePosition, targetX: this.targetX, targetY: this.targetY, + sourcePosition: this.sourcePosition, + targetPosition: this.targetPosition, centerX: (this.sourceX + this.targetX) / 2, centerY: (this.sourceY + this.targetY) / 2, - targetPosition: this.targetPosition, borderRadius: 10, offset: 10, }); @@ -132,29 +219,95 @@ export default { return this.edgeData[0]; }, labelX() { - return this.edgeData[1] + ((this.data?.offsetIndex < 0 ? this.data?.offsetIndex : 0) || 0) * 100; + return this.edgeData[1] + ((this.data?.offsetIndex < 0 ? this.data?.offsetIndex : 0) || 0) * this.maxWidth; }, labelY() { return this.edgeData[2] + ((this.data?.offsetIndex > 0 ? this.data?.offsetIndex : 0) || 0) * 75; }, - }, - methods: { - onEdgeKeydown(e) { - if (e.key === 'Enter' || e.key === ' ') { - this.data.onSelect(); - e.preventDefault(); + menuItems() { + const items = []; + if (this.data?.permissions?.edit && this.$refs.editButton) { + items.push(this.$refs.editButton); } - if ((e.key === 'e' || e.key === 'E') && this.data?.isTransitionMode) { - this.data.onEdit(); - e.preventDefault(); + if (this.data?.permissions?.delete && this.$refs.deleteButton) { + items.push(this.$refs.deleteButton); } - if ((e.key === 'Delete' || e.key === 'Backspace') && this.data?.isTransitionMode) { - this.data.onDelete?.(); - e.preventDefault(); + return items; + }, + }, + watch: { + 'data.title': { + handler: 'updateLabelWidth', + immediate: true, + }, + showActions(newVal) { + if (!newVal) { + this.currentMenuIndex = -1; + } + }, + }, + mounted() { + this.updateLabelWidth(); + }, + beforeUnmount() { + if (this.blurTimeout) clearTimeout(this.blurTimeout); + if (this.hoverTimeout) clearTimeout(this.hoverTimeout); + }, + methods: { + toggleActions() { + this.showActions = !this.showActions; + if (this.showActions) { + this.$nextTick(() => this.focusFirstMenuItem()); } }, - onEdgeFocus() {}, - onEdgeBlur() {}, + openActions() { + this.data.onSelect?.(); + this.showActions = true; + this.isHovered = false; + }, + closeActions() { + this.data.onEscape?.(); + clearTimeout(this.hoverTimeout); + clearTimeout(this.blurTimeout); + this.showActions = false; + }, + handleEdit() { + this.closeActions(); + this.data?.onEdit?.(); + }, + handleDelete() { + this.closeActions(); + this.data?.onDelete?.(); + }, + onNodeEnter() { + clearTimeout(this.hoverTimeout); + this.isHovered = true; + }, + onNodeLeave() { + this.hoverTimeout = setTimeout(() => { + if (!this.showActions) this.isHovered = false; + }, 100); + }, + onDropdownEnter() { + clearTimeout(this.blurTimeout); + }, + onDropdownLeave() { + this.blurTimeout = setTimeout(() => { + this.closeActions(); + this.isHovered = false; + }, 100); + }, + onSelected() { + return this.data.onSelect; + }, + updateLabelWidth() { + this.$nextTick(() => { + if (this.$refs.textMeasurer) { + const measuredWidth = this.$refs.textMeasurer.offsetWidth; + this.maxWidth = Math.min(measuredWidth + 50, 300); + } + }); + }, }, }; diff --git a/administrator/components/com_workflow/resources/scripts/components/nodes/StageNode.vue b/administrator/components/com_workflow/resources/scripts/components/nodes/StageNode.vue index 10b716eb90a..ea4cfd729cf 100644 --- a/administrator/components/com_workflow/resources/scripts/components/nodes/StageNode.vue +++ b/administrator/components/com_workflow/resources/scripts/components/nodes/StageNode.vue @@ -6,87 +6,151 @@ tabindex="0" :data-stage-id="stage?.id" role="button" - :aria-label="`${translate('COM_WORKFLOW_GRAPH_STAGE')} : - ${stage?.title}. ${stage?.published ? translate('WORKFLOW_GRAPH_ENABLED') : - translate('WORKFLOW_GRAPH_DISABLED')}`" - @keydown="onNodeKeydown" - @focus="onNodeFocus" - @blur="onNodeBlur" + @mouseenter="onNodeEnter" + @mouseleave="onNodeLeave" + @focus="onNodeEnter" + @blur="onNodeLeave" @click="onSelected" + @keydown.enter="openActions" + @keydown.esc="closeActions" + @keydown.tab="closeActions" > - + + + + + + + {{ stage.title }} + + + + {{ translate('COM_WORKFLOW_GRAPH_EDIT_STAGE') }} + + + + + {{ translate('COM_WORKFLOW_GRAPH_DELETE_STAGE_TITLE') }} + + + + - - + + {{ stage.title }} {{ stage.description }} - + - - - - + + {{ stage.published ? translate('COM_WORKFLOW_GRAPH_ENABLED') : translate('COM_WORKFLOW_GRAPH_DISABLED') }} - import { Handle, Position } from '@vue-flow/core'; -import { focusNode } from '../../utils/focus-utils.es6'; export default { name: 'StageNode', @@ -128,6 +190,14 @@ export default { }, }, emits: ['navigate'], + data() { + return { + showActions: false, + isHoveredOrFocused: false, + hoverTimeout: null, + blurTimeout: null, + }; + }, computed: { Position() { return Position; @@ -140,46 +210,58 @@ export default { }, stageStyle() { return { - borderColor: `${this.data.stage.color}!important`, - borderWidth: this.data.isSelected ? '4px !important' : '2px !important', + borderColor: `${this.stage.color} !important`, + borderWidth: this.isSelected ? '4px !important' : '0 !important', + background: this.data.isSpecial ? 'purple !important' : 'rgb(var(--primary-rgb)) !important', }; }, badgeStyle() { - return { backgroundColor: this.data.stage.color }; + return { backgroundColor: this.stage.color }; }, onSelected() { return this.data.onSelect; }, + onEscape() { + return this.data.onEscape; + }, }, methods: { - onNodeKeydown(e) { - if (e.key === 'Enter' || e.key === ' ') { - this.data.onSelect(); - e.preventDefault(); - } - if (e.key === 'e' || e.key === 'E') { - if (this.data.isSpecial) { - return; - } - this.data.onEdit(); - e.preventDefault(); - } - if (e.key === 'Delete' || e.key === 'Backspace') { - if (this.data.isSpecial) { - return; - } - this.data.onDelete(); - e.preventDefault(); - } - if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key)) { - this.$emit('navigate', e.key); - e.preventDefault(); - } - }, - onNodeFocus(node) { - focusNode(node?.id); - }, - onNodeBlur() {}, + openActions() { + this.data.onSelect?.(); + this.showActions = true; + this.isHoveredOrFocused = false; + }, + closeActions() { + this.data.onEscape?.(); + clearTimeout(this.hoverTimeout); + clearTimeout(this.blurTimeout); + this.showActions = false; + }, + handleEdit() { + this.closeActions(); + this.data?.onEdit?.(); + }, + handleDelete() { + this.closeActions(); + this.data?.onDelete?.(); + }, + onNodeEnter() { + clearTimeout(this.hoverTimeout); + this.isHoveredOrFocused = true; + }, + onNodeLeave() { + this.hoverTimeout = setTimeout(() => { + if (!this.showActions) this.isHoveredOrFocused = false; + }, 100); + }, + onDropdownEnter() { + clearTimeout(this.blurTimeout); + }, + onDropdownLeave() { + this.blurTimeout = setTimeout(() => { + this.closeActions(); + }, 100); + }, }, }; diff --git a/administrator/components/com_workflow/resources/scripts/plugins/Notifications.es6.js b/administrator/components/com_workflow/resources/scripts/plugins/Notifications.es6.js new file mode 100644 index 00000000000..d12b27ee9cc --- /dev/null +++ b/administrator/components/com_workflow/resources/scripts/plugins/Notifications.es6.js @@ -0,0 +1,54 @@ +/** + * Send a notification + * @param {String} message + * @param {{}} options + * + */ +function notify(message, options) { + let timer; + if (options.type === 'message') { + timer = 3000; + } + Joomla.renderMessages( + { + [options.type]: [Joomla.Text._(message)], + }, + undefined, + true, + timer, + ); +} + +const notifications = { + /* Send a success notification */ + success: (message, options) => { + notify(message, { + type: 'message', // @todo rename it to success + dismiss: true, + ...options, + }); + }, + + /* Send an error notification */ + error: (message, options) => { + notify(message, { + type: 'error', // @todo rename it to danger + dismiss: true, + ...options, + }); + }, + + /* Send a general notification */ + notify: (message, options) => { + notify(message, { + type: 'message', + dismiss: true, + ...options, + }); + }, + + /* Ask the user a question */ + ask: (message) => window.confirm(message), +}; + +export default notifications; diff --git a/administrator/components/com_workflow/resources/scripts/store/actions.es6.js b/administrator/components/com_workflow/resources/scripts/store/actions.es6.js index 7af79e2a405..2b062060586 100644 --- a/administrator/components/com_workflow/resources/scripts/store/actions.es6.js +++ b/administrator/components/com_workflow/resources/scripts/store/actions.es6.js @@ -1,4 +1,5 @@ import workflowGraphApi from '../app/WorkflowGraphApi.es6.js'; +import notifications from '../plugins/Notifications.es6'; /** * Vuex Actions for asynchronous operations and workflows @@ -23,10 +24,10 @@ export default { await workflowGraphApi.getTransitions(id), ]); + commit('SET_WORKFLOW_ID', id); commit('SET_WORKFLOW', workflowRes?.data); commit('SET_STAGES', stagesRes?.data); commit('SET_TRANSITIONS', transitionsRes?.data); - commit('SET_WORKFLOW_ID', id); dispatch('saveToHistory'); } catch (error) { @@ -60,9 +61,7 @@ export default { ) { const errorMessage = 'COM_WORKFLOW_ERROR_STAGE_DEFAULT_CANT_DELETED'; commit('SET_ERROR', errorMessage); - if (window.Joomla && window.Joomla.renderMessages) { - window.Joomla.renderMessages({ error: [errorMessage] }); - } + notifications.error(errorMessage); return; } diff --git a/administrator/components/com_workflow/resources/scripts/utils/edges.es6.js b/administrator/components/com_workflow/resources/scripts/utils/edges.es6.js index 3d4153c3cd2..61ca2975a68 100644 --- a/administrator/components/com_workflow/resources/scripts/utils/edges.es6.js +++ b/administrator/components/com_workflow/resources/scripts/utils/edges.es6.js @@ -4,13 +4,11 @@ import { getEdgeColor } from './utils.es6.js'; * Generate styled edges based on transition data. * @param {Array} transitions - List of transitions. * @param {Object} options - Optional configuration. - * @param {Boolean} options.transitionMode - Whether transition mode is enabled. * @param {Number|String|null} options.selectedId - Currently selected transition id. * @returns {Array} Styled edge definitions. */ export function generateStyledEdges(transitions, options = {}) { const { - transitionMode = false, selectedId = null, } = options; @@ -51,14 +49,12 @@ export function generateStyledEdges(transitions, options = {}) { }, data: { ...transition, - isTransitionMode: transitionMode, isSelected, isBiDirectional, offsetIndex, onEdit: () => {}, onDelete: () => {}, }, - draggable: !transitionMode, }; }); } diff --git a/administrator/components/com_workflow/resources/scripts/utils/keyboard-manager.es6.js b/administrator/components/com_workflow/resources/scripts/utils/keyboard-manager.es6.js index b13f3dc547b..878e2a08004 100644 --- a/administrator/components/com_workflow/resources/scripts/utils/keyboard-manager.es6.js +++ b/administrator/components/com_workflow/resources/scripts/utils/keyboard-manager.es6.js @@ -20,9 +20,9 @@ import { */ export function setupGlobalShortcuts({ addStage, addTransition, editItem, deleteItem, - toggleMode, undo, redo, updateSaveMessage, - saveNodePosition, clearSelection, zoomIn, - zoomOut, fitView, viewport, state, setSaveStatus, store, + undo, redo, updateSaveMessage, saveNodePosition, + clearSelection, zoomIn, zoomOut, fitView, + viewport, state, setSaveStatus, store, }) { function isModifierPressed(e, key) { return (e.ctrlKey || e.metaKey) && [key.toLowerCase(), key.toUpperCase()].includes(e.key); @@ -89,29 +89,24 @@ export function setupGlobalShortcuts({ announce(state.liveRegion, 'Add transition'); break; - case e.altKey && ['u', 'U'].includes(e.key): + case isModifierPressed(e, 'z'): e.preventDefault(); - editItem(); + undo(); break; - case e.altKey && e.shiftKey && ['d', 'D'].includes(e.key): + case isModifierPressed(e, 'y'): e.preventDefault(); - deleteItem(); + redo(); break; - case e.altKey && ['c', 'C'].includes(e.key): + case e.key === 'e' || e.key === 'E': e.preventDefault(); - toggleMode(); - break; - - case isModifierPressed(e, 'z'): - e.preventDefault(); - undo(); + editItem(); break; - case isModifierPressed(e, 'y'): + case e.key === 'Delete' || e.key === 'Backspace': e.preventDefault(); - redo(); + deleteItem(); break; case e.key === 'Escape': @@ -148,7 +143,19 @@ export function setupGlobalShortcuts({ case ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key): e.preventDefault(); if (state.selectedStage.value) { - moveNode(state.selectedStage.value.toString(), e.key, e.shiftKey); + if (e.shiftKey) { + moveNode(state.selectedStage.value.toString(), e.key, e.shiftKey); + } else { + const buttonSelector = `.stage-node[data-stage-id='${state.selectedStage.value}'] button[tabindex="0"]`; + if (buttonSelector) { + cycleFocus(buttonSelector, 0); + } + } + } else if (state.selectedTransition.value) { + const buttonSelector = `.edge-label[data-edge-id='${state.selectedTransition.value}'] button[tabindex="0"]`; + if (buttonSelector) { + cycleFocus(buttonSelector, 0); + } } else if (e.shiftKey) { const panStep = 20; switch (e.key) { diff --git a/administrator/components/com_workflow/resources/scripts/utils/utils.es6.js b/administrator/components/com_workflow/resources/scripts/utils/utils.es6.js index bff6b78fa8a..29f8e18b465 100644 --- a/administrator/components/com_workflow/resources/scripts/utils/utils.es6.js +++ b/administrator/components/com_workflow/resources/scripts/utils/utils.es6.js @@ -27,8 +27,8 @@ export function getColorForTransition(transition) { * @returns {string} Hex or HSL color. */ export function getEdgeColor(transition, isSelected) { - if (isSelected) return '#3B82F6'; // Blue for selected - if (transition?.published) return getColorForTransition(transition); + if (isSelected) return getColorForTransition(transition); // Blue for selected + if (transition?.published) return '#3B82F6'; return (transition.from_stage_id === -1 || transition.to_stage_id === -1) ? '#F97316' : '#10B981'; } diff --git a/administrator/components/com_workflow/resources/scripts/workflowgraph.es6.js b/administrator/components/com_workflow/resources/scripts/workflowgraph.es6.js index eb3515390d1..2adcd31aaa2 100644 --- a/administrator/components/com_workflow/resources/scripts/workflowgraph.es6.js +++ b/administrator/components/com_workflow/resources/scripts/workflowgraph.es6.js @@ -3,6 +3,7 @@ import App from './components/App.vue'; import EventBus from './app/Event.es6'; import store from './store/store.es6'; import translate from './plugins/translate.es6.js'; +import notifications from './plugins/Notifications.es6.js'; // Register WorkflowGraph namespace window.WorkflowGraph = window.WorkflowGraph || {}; @@ -10,11 +11,6 @@ window.WorkflowGraph = window.WorkflowGraph || {}; window.WorkflowGraph.Event = EventBus; document.addEventListener('DOMContentLoaded', () => { - const skipToButton = document.querySelector('.skip-to button'); - if (skipToButton) { - skipToButton.focus(); - } - const mountElement = document.getElementById('workflow-graph-root'); if (mountElement) { @@ -22,9 +18,7 @@ document.addEventListener('DOMContentLoaded', () => { app.use(store); app.use(translate); app.mount(mountElement); - } else if (window.Joomla && window.Joomla.renderMessages) { - window.Joomla.renderMessages({ - error: ['Mount element #workflow-graph-root not found'], - }); + } else { + notifications.error('Can\'t start the page, the root is not found'); } }); diff --git a/administrator/components/com_workflow/src/Controller/StageController.php b/administrator/components/com_workflow/src/Controller/StageController.php index d2f6d7c7166..ad3c423d46a 100644 --- a/administrator/components/com_workflow/src/Controller/StageController.php +++ b/administrator/components/com_workflow/src/Controller/StageController.php @@ -177,12 +177,12 @@ protected function getRedirectToListAppend() } /** - * Method to save a request. + * Method to save a record. * - * @param string $key The name of the primary key of the URL variable. + * @param string $key The name of the primary key of the URL variable. * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). * - * @return boolean True if access level checks pass, false otherwise. + * @return boolean True if successful, false otherwise. * * @since __DEPLOY_VERSION__ */ diff --git a/administrator/components/com_workflow/src/View/Graph/HtmlView.php b/administrator/components/com_workflow/src/View/Graph/HtmlView.php index d99f710d6be..e5dfb1e3cb5 100644 --- a/administrator/components/com_workflow/src/View/Graph/HtmlView.php +++ b/administrator/components/com_workflow/src/View/Graph/HtmlView.php @@ -86,13 +86,11 @@ public function display($tpl = null) $model = $this->getModel(); // Get the data - $this->state = $model->getState(); - $this->item = $model->getItem(); - - - // Check for errors. - if (\count($errors = $model->getErrors())) { - throw new GenericDataException(implode("\n", $errors), 500); + try { + $this->state = $model->getState(); + $this->item = $model->getItem(); + } catch (\Exception $e) { + throw new GenericDataException(Text::_('COM_WORKFLOW_GRAPH_ERROR_FETCHING_MODEL') . $e->getMessage(), 500, $e); } $extension = $this->state->get('filter.extension'); diff --git a/administrator/components/com_workflow/tmpl/workflows/default.php b/administrator/components/com_workflow/tmpl/workflows/default.php index 7ee58667588..33070384fc7 100644 --- a/administrator/components/com_workflow/tmpl/workflows/default.php +++ b/administrator/components/com_workflow/tmpl/workflows/default.php @@ -180,7 +180,7 @@ - + diff --git a/build/media_source/com_workflow/scss/components/_workflow-graph-custom.scss b/build/media_source/com_workflow/scss/components/_workflow-graph-custom.scss index 416ceb2f9e4..15ba5ed60b6 100644 --- a/build/media_source/com_workflow/scss/components/_workflow-graph-custom.scss +++ b/build/media_source/com_workflow/scss/components/_workflow-graph-custom.scss @@ -4,10 +4,6 @@ background-size: 20px 20px; } -.vue-flow__panel { - z-index: 10; -} - .min-vh-80 { height: 80vh; } @@ -20,6 +16,14 @@ height: 40px; } +.z-10 { + z-index: 10; +} + +.z-20 { + z-index: 20; +} + /* ----- Edges ----- */ .vue-flow__edge-path { stroke-width: 2; @@ -84,6 +88,11 @@ } /* ----- Card Actions ----- */ +.workflow-browser-actions-list { + background-color: #000; + box-shadow: 0 4px 10px rgba(0, 0, 0, .6); +} + .stage-card-actions button { --btn-padding-x: 2px !important; --btn-padding-y: 0 !important; @@ -98,14 +107,6 @@ } /* ----- Accessibility ----- */ -.stage-node:focus, .stage-node:focus-visible, -.edge-label:focus, .edge-label:focus-visible, -button:focus, button:focus-visible, -:focus-visible, :focus:not(:disabled):not([readonly]) { - border: 2px solid var(--template-link-color) !important; - box-shadow: 0 0 0 .25rem rgba(42, 105, 183, .25) !important; -} - .sr-only, .visually-hidden { position: absolute; @@ -123,7 +124,6 @@ button:focus, button:focus-visible, position: absolute; right: 20px; bottom: 20px; - z-index: 5; display: flex; flex-direction: column; gap: 1px;
{{ stage.description }}