Skip to content

Commit d5e2359

Browse files
committed
permissions added
1 parent 38a0912 commit d5e2359

File tree

3 files changed

+206
-43
lines changed

3 files changed

+206
-43
lines changed

administrator/components/com_workflow/src/Controller/GraphController.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,7 @@ public function getTransitions()
257257
foreach ($transitions as $transition) {
258258
$canEdit = $user->authorise('core.edit', $this->extension . '.transition.' . (int) $transition->id);
259259
$canDelete = $user->authorise('core.delete', $this->extension . '.transition.' . (int) $transition->id);
260+
$canRun = $user->authorise('core.execute.transition', $this->extension . '.transition.' . (int) $transition->id);
260261

261262
$response[] = [
262263
'id' => (int) $transition->id,
@@ -270,6 +271,7 @@ public function getTransitions()
270271
'permissions' => [
271272
'edit' => $canEdit,
272273
'delete' => $canDelete,
274+
'run_transition' => $canRun
273275
],
274276
];
275277
}

build/media_source/com_workflow/js/workflow-graph-client.es6.js

Lines changed: 161 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,45 @@ Joomla = window.Joomla || {};
3333
return makeRequest(`&task=graph.getTransitions&workflow_id=${workflowId}&format=json`);
3434
}
3535

36+
function filterWorkflow(stages, transitions) {
37+
// Step 1: Filter transitions by run permission
38+
let filteredTransitions = transitions.filter(tr => tr.permissions?.run_transition);
39+
40+
// Step 2: Collect stage IDs that are connected by accessible transitions
41+
const connectedStageIds = new Set();
42+
filteredTransitions.forEach(tr => {
43+
if (tr.from_stage_id !== -1) connectedStageIds.add(tr.from_stage_id);
44+
connectedStageIds.add(tr.to_stage_id);
45+
});
46+
47+
// Step 3: Filter stages by edit/delete permission OR connectivity
48+
let filteredStages = stages.filter(st => {
49+
const editable = st.permissions?.edit || st.permissions?.delete;
50+
const connected = connectedStageIds.has(st.id);
51+
return editable || connected;
52+
});
53+
54+
// Step 4: Remove transitions pointing to removed stages
55+
const validStageIds = new Set(filteredStages.map(st => st.id));
56+
filteredTransitions = filteredTransitions.filter(tr =>
57+
(tr.from_stage_id === -1 || validStageIds.has(tr.from_stage_id)) &&
58+
validStageIds.has(tr.to_stage_id)
59+
);
60+
61+
return { stages: filteredStages, transitions: filteredTransitions };
62+
}
63+
64+
3665
function calculateAutoLayout(stages, transitions) {
3766
const needsPosition = stages.filter((stage) => !stage.position || isNaN(stage.position.x) || isNaN(stage.position.y));
3867
if (needsPosition.length === 0) return stages;
3968

69+
// Place "From Any" at fixed position if present
70+
const fromAnyStage = stages.find((s) => s.id === 'From Any');
71+
if (fromAnyStage && (!fromAnyStage.position || isNaN(fromAnyStage.position.x) || isNaN(fromAnyStage.position.y))) {
72+
fromAnyStage.position = { x: 600, y: -200 };
73+
}
74+
4075
const outgoing = new Map(stages.map((s) => [s.id, []]));
4176
const inDegree = new Map(stages.map((s) => [s.id, 0]));
4277

@@ -50,14 +85,14 @@ Joomla = window.Joomla || {};
5085
});
5186

5287
const levels = [];
53-
let queue = stages.filter((s) => inDegree.get(s.id) === 0);
88+
let queue = stages.filter((s) => inDegree.get(s.id) === 0 && s.id !== 'From Any');
5489
while (queue.length > 0) {
5590
levels.push(queue);
5691
const nextQueue = [];
5792
for (const stage of queue) {
5893
for (const targetId of outgoing.get(stage.id) || []) {
5994
inDegree.set(targetId, inDegree.get(targetId) - 1);
60-
if (inDegree.get(targetId) === 0) {
95+
if (inDegree.get(targetId) === 0 && targetId !== 'From Any') {
6196
nextQueue.push(stages.find((s) => s.id === targetId));
6297
}
6398
}
@@ -90,7 +125,7 @@ Joomla = window.Joomla || {};
90125
stage.position.y = parseFloat(stage.position.y);
91126
}
92127
});
93-
128+
94129
const hasStart = transitions.some((tr) => tr.from_stage_id === -1);
95130
if (hasStart && !stages.find((s) => s.id === 'From Any')) stages.unshift({ id: 'From Any', title: 'From Any' });
96131

@@ -104,9 +139,14 @@ Joomla = window.Joomla || {};
104139
data: stage,
105140
className: `stage ${stage.default ? 'default' : ''} ${isVirtual ? 'virtual' : ''}`,
106141
innerHTML: `
107-
<div class="stage-title">${stage.title}</div>
108-
${stage.description ? `<div class="stage-description">${stage.description}</div>` : ''}
109-
${stage.default ? '<div class="badge bg-warning bg-opacity-10 rounded-pill p-1">DEFAULT</div>' : ''}
142+
<div class="stage-title text-truncate" style="max-width: 180px;" title="${stage.title}">${stage.title}</div>
143+
${stage.description ? `<div class="stage-description text-truncate small text-white" style="max-width: 180px;" title="${stage.description}">${stage.description}</div>` : ''}
144+
<div style="display: flex; gap: 4px; align-items: center; margin-top: 2px;">
145+
${stage.default ? '<div class="badge bg-warning bg-opacity-10 rounded-pill p-1">DEFAULT</div>' : ''}
146+
${typeof stage.published !== 'undefined'
147+
? `<div class="badge ${stage.published == 1 ? 'bg-success' : 'bg-warning'} rounded-pill p-1">${stage.published == 1 ? 'ENABLED' : 'DISABLED'}</div>`
148+
: ''}
149+
</div>
110150
`,
111151
};
112152
});
@@ -117,16 +157,16 @@ Joomla = window.Joomla || {};
117157
*/
118158
function generateEdges(transitions, stages) {
119159
const STAGE_WIDTH = 200;
120-
const STAGE_HEIGHT = 80;
160+
const STAGE_HEIGHT = 100;
121161

122162
const getConnectionPoint = (fromStage, toStage, isSource) => {
123163
const node = isSource ? fromStage : toStage;
124164
const center = { x: node.position.x + STAGE_WIDTH / 2, y: node.position.y + STAGE_HEIGHT / 2 };
125-
const otherCenter = {
126-
x: (isSource ? toStage : fromStage).position.x + STAGE_WIDTH / 2,
127-
y: (isSource ? toStage : fromStage).position.y + STAGE_HEIGHT / 2
165+
const otherCenter = {
166+
x: (isSource ? toStage : fromStage).position.x + STAGE_WIDTH / 2,
167+
y: (isSource ? toStage : fromStage).position.y + STAGE_HEIGHT / 2
128168
};
129-
169+
130170
const dx = otherCenter.x - center.x;
131171
const dy = otherCenter.y - center.y;
132172

@@ -174,8 +214,6 @@ Joomla = window.Joomla || {};
174214

175215
function renderNodes(nodes, container, onDrag) {
176216
container.innerHTML = '';
177-
const STAGE_WIDTH = 200;
178-
const STAGE_HEIGHT = 80;
179217

180218
nodes.forEach((node) => {
181219
const div = document.createElement('div');
@@ -184,39 +222,65 @@ Joomla = window.Joomla || {};
184222
div.innerHTML = node.innerHTML;
185223
div.style.left = `${node.position.x}px`;
186224
div.style.top = `${node.position.y}px`;
187-
225+
188226
div.addEventListener('mousedown', (e) => {
189227
if (e.button !== 0) return;
190228
e.stopPropagation();
191229
onDrag(e, node.data);
192230
});
193-
231+
194232
container.appendChild(div);
195233
});
196234
}
197-
235+
236+
function highlightTransition(edgeId) {
237+
// Reset all first
238+
document.querySelectorAll('.transition-path').forEach(p => {
239+
p.classList.remove('highlighted');
240+
});
241+
document.querySelectorAll('.transition-label-content').forEach(l => {
242+
l.classList.remove('highlighted');
243+
});
244+
245+
// Highlight selected
246+
const path = document.querySelector(`.transition-path[data-edge-id="${edgeId}"]`);
247+
const label = document.querySelector(`.transition-label-content[data-edge-id="${edgeId}"]`);
248+
249+
if (path) path.classList.add('highlighted');
250+
if (label) label.classList.add('highlighted');
251+
}
252+
253+
198254
function renderEdges(edges, svg) {
199255
svg.querySelectorAll('path, foreignObject').forEach((el) => el.remove());
200-
256+
201257
edges.forEach((edge) => {
202258
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
203259
path.setAttribute('d', edge.pathData);
204260
path.setAttribute('class', 'transition-path');
261+
path.setAttribute('data-edge-id', edge.id); // track edge
205262
path.setAttribute('marker-end', 'url(#arrowhead)');
206-
svg.appendChild(path);
207263

208264
const foreignObject = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
209265
foreignObject.setAttribute('class', 'transition-label');
210266
foreignObject.setAttribute('width', '120');
211267
foreignObject.setAttribute('height', '24');
212268
foreignObject.setAttribute('x', edge.labelPosition.x - 60);
213269
foreignObject.setAttribute('y', edge.labelPosition.y - 12);
214-
270+
215271
const labelDiv = document.createElement('div');
216272
labelDiv.className = 'transition-label-content';
217273
labelDiv.textContent = edge.label;
274+
labelDiv.dataset.edgeId = edge.id;
275+
labelDiv.addEventListener('click', (e) => {
276+
e.stopPropagation();
277+
highlightTransition(edge.id);
278+
});
279+
218280
foreignObject.appendChild(labelDiv);
281+
svg.appendChild(path);
219282
svg.appendChild(foreignObject);
283+
220284
});
221285
}
222286

@@ -249,16 +313,16 @@ Joomla = window.Joomla || {};
249313
// Pan & Zoom state
250314
svg.innerHTML = `
251315
<defs>
252-
<marker
253-
id="arrowhead"
254-
markerWidth="12"
255-
markerHeight="12"
256-
refX="11"
257-
refY="6"
258-
orient="auto"
259-
markerUnits="strokeWidth">
260-
<polygon points="0 0, 12 6, 0 12" class="arrow-marker" />
261-
</marker>
316+
<marker
317+
id="arrowhead"
318+
markerWidth="8"
319+
markerHeight="8"
320+
refX="7"
321+
refY="4"
322+
orient="auto"
323+
markerUnits="strokeWidth">
324+
<polygon points="0 0, 8 4, 0 8" class="arrow-marker" />
325+
</marker>
262326
</defs>`;
263327
let state = { stages: [], transitions: [], scale: 1, panX: 0, panY: 0, isDraggingStage: false };
264328

@@ -280,18 +344,18 @@ Joomla = window.Joomla || {};
280344
stageY: draggedStage.position.y,
281345
};
282346
stageElement.classList.add('dragging');
283-
347+
284348
const onMouseMove = (moveEvent) => {
285349
draggedStage.position.x = dragStart.stageX + (moveEvent.clientX - dragStart.x) / state.scale;
286350
draggedStage.position.y = dragStart.stageY + (moveEvent.clientY - dragStart.y) / state.scale;
287-
351+
288352
stageElement.style.left = `${draggedStage.position.x}px`;
289353
stageElement.style.top = `${draggedStage.position.y}px`;
290354

291355
const edges = generateEdges(state.transitions, state.stages);
292356
renderEdges(edges, svg);
293357
};
294-
358+
295359
const onMouseUp = () => {
296360
document.removeEventListener('mousemove', onMouseMove);
297361
document.removeEventListener('mouseup', onMouseUp);
@@ -347,16 +411,25 @@ Joomla = window.Joomla || {};
347411

348412
updateTransform();
349413
}
350-
414+
351415

352416
Promise.all([
353417
getWorkflow(workflowId),
354418
getStages(workflowId),
355419
getTransitions(workflowId)
356420
]).then(([workflowData, stagesData, transitionsData]) => {
357421
const workflow = workflowData?.data || {};
358-
state.stages = stagesData?.data || [];
359-
state.transitions = transitionsData?.data || [];
422+
let stages = stagesData?.data || [];
423+
let transitions = transitionsData?.data || [];
424+
425+
({ stages, transitions } = filterWorkflow(stages, transitions));
426+
427+
console.log('stages:', stages);
428+
console.log('transitions:', transitions);
429+
state.stages = stages;
430+
state.transitions = transitions;
431+
432+
360433

361434
if (!state.stages.length) {
362435
stageContainer.innerHTML = "<p>No stages defined.</p>";
@@ -396,9 +469,59 @@ Joomla = window.Joomla || {};
396469
zoomControls = document.createElement('div');
397470
zoomControls.className = 'zoom-controls';
398471
zoomControls.innerHTML = `
399-
<button class="zoom-btn zoom-in" title="Zoom In (+)">+</button>
400-
<button class="zoom-btn zoom-out" title="Zoom Out (-)">−</button>
401-
<button class="zoom-btn fit-screen" title="Fit to Screen (F)">⌂</button>
472+
<div
473+
ref="controlsContainer"
474+
class="custom-controls z-10"
475+
role="group"
476+
aria-labelledby="canvas-controls-title"
477+
>
478+
<h2 id="canvas-controls-title" class="visually-hidden">Canvas View Controls</h2>
479+
480+
<ul class="d-flex flex-column gap-1 list-unstyled mb-0" role="group">
481+
<li>
482+
<button
483+
class="zoom-btn zoom-in"
484+
tabindex="0"
485+
type="button"
486+
aria-label="Zoom in"
487+
title="Zoom in (+ key)"
488+
>
489+
<span class="icon icon-plus" aria-hidden="true" />
490+
<span class="visually-hidden">Zoom In</span>
491+
</button>
492+
</li>
493+
<li>
494+
<button
495+
class="zoom-btn zoom-out"
496+
tabindex="0"
497+
type="button"
498+
aria-label="Zoom out"
499+
title="Zoom out (- key)"
500+
@click="zoomOut"
501+
@keydown.enter="zoomOut"
502+
@keydown.space.prevent="zoomOut"
503+
>
504+
<span class="icon icon-minus" aria-hidden="true" />
505+
<span class="visually-hidden">Zoom Out</span>
506+
</button>
507+
</li>
508+
<li>
509+
<button
510+
class="zoom-btn fit-screen"
511+
tabindex="0"
512+
type="button"
513+
aria-label="Fit view"
514+
title="Fit view (F key)"
515+
@click="customFitView"
516+
@keydown.enter="customFitView"
517+
@keydown.space.prevent="customFitView"
518+
>
519+
<span class="icon icon-expand" aria-hidden="true" />
520+
<span class="visually-hidden">Fit View</span>
521+
</button>
522+
</li>
523+
</ul>
524+
</div>
402525
`;
403526
container.appendChild(zoomControls);
404527

0 commit comments

Comments
 (0)