001/* 002 * Copyright 2010 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.ametys.cms.clientsideelement; 017 018import java.util.ArrayList; 019import java.util.Arrays; 020import java.util.HashMap; 021import java.util.List; 022import java.util.Map; 023import java.util.stream.Collectors; 024 025import org.apache.avalon.framework.component.ComponentException; 026import org.apache.avalon.framework.configuration.Configuration; 027import org.apache.avalon.framework.configuration.ConfigurationException; 028import org.apache.avalon.framework.configuration.DefaultConfiguration; 029import org.apache.avalon.framework.service.ServiceException; 030import org.apache.avalon.framework.service.ServiceManager; 031import org.apache.commons.collections.ListUtils; 032import org.slf4j.LoggerFactory; 033 034import org.ametys.cms.content.ContentHelper; 035import org.ametys.cms.repository.Content; 036import org.ametys.cms.repository.WorkflowAwareContent; 037import org.ametys.cms.workflow.AbstractContentWorkflowComponent; 038import org.ametys.core.ui.Callable; 039import org.ametys.core.ui.ClientSideElement; 040import org.ametys.core.ui.MenuClientSideElement; 041import org.ametys.core.ui.StaticClientSideElement; 042import org.ametys.core.ui.StaticFileImportsClientSideElement; 043import org.ametys.core.user.User; 044import org.ametys.core.user.UserIdentity; 045import org.ametys.core.user.UserManager; 046import org.ametys.plugins.core.ui.util.ConfigurationHelper; 047import org.ametys.plugins.repository.AmetysObjectResolver; 048import org.ametys.plugins.repository.lock.LockHelper; 049import org.ametys.plugins.repository.lock.LockableAmetysObject; 050import org.ametys.plugins.workflow.AbstractWorkflowComponent; 051import org.ametys.plugins.workflow.support.WorkflowHelper; 052import org.ametys.plugins.workflow.support.WorkflowProvider; 053import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow; 054import org.ametys.runtime.i18n.I18nizableText; 055import org.ametys.runtime.plugin.component.ThreadSafeComponentManager; 056 057import com.opensymphony.workflow.loader.ActionDescriptor; 058import com.opensymphony.workflow.loader.StepDescriptor; 059import com.opensymphony.workflow.loader.WorkflowDescriptor; 060import com.opensymphony.workflow.spi.Step; 061 062/** 063 * This element creates multiple toggle buttons representing a workflow. 064 * A menu represent workflow actions. 065 * 066 * The awaited configuration is: 067 * <workflow name="WORKFLOWNAME"> 068 * <action> 069 * <menu-1-label>I18N_KEY</menu-1-label> 070 * </action> 071 * 072 * <workflow-actions mode="exclude"> 073 * <action>22</action> 074 * <action>222</action> 075 * </workflow-actions> 076 * 077 * <workflow-steps mode="include"> 078 * <step>1</step> 079 * <step>2</step> 080 * </workflow-steps> 081 * 082 * <steps> 083 * <step id="1"> 084 * <workflow-actions mode="include"> 085 * <action>2</action> 086 * </workflow-actions> 087 * 088 * <comments mode="include"> 089 * <action>3</action> 090 * </comments> 091 * 092 * <action> 093 * <menu-1-label /> 094 * </action> 095 * </step> 096 * 097 * </steps> 098 * </workflow> 099 * 100 * Where WORKFLOWNAME is the name of the workflow such as 'content'. MANDATORY 101 * Where <action> is optional to override the default configuration of every step button and step menu 102 * The attribute name="..." is optional to specify the JS class used. 103 * Where <workflow-actions< is optional to restrict the available actions in the menu. Default value is <workflow-actions mode="exclude"/> 104 * The attribute mode="include" indicates that the list of actions ids will replace the one automatically determined (even if it only can by a sublist). Listing ids here ensure that no new actions will appear in this menu if the workflow is modified. 105 * The attribute mode="exclude" (default value) indicates that the list of actions ids will be removed from the one automatically determined (it only can by a sublist of it). Listing ids here ensure that when the workflow has a new action it will be added here. 106 * Where <steps> is optional and allows you to configure each step specifically. 107 * The <step> id attribute is MANDATORY 108 * The <workflow-actions> is identical to the one at the root of <workflow> but only for this specific step 109 * Where <comments< is identical to <workflow-actions< but to display to the user a dialog box to enter comments. The workflow has to support comments on that action. To avoid comments, set <comments mode="include"/> 110 * The <action> is identical to the one at the root of <workflow> but only for this specific step 111 * 112 * 113 * Here is a declaration sample in a plugin.xml file 114 * 115 * <extension id="org.ametys.cms.workflow.WorkflowSteps" 116 * point="org.ametys.core.ui.RibbonControlsManager" 117 * class="org.ametys.cms.clientsideelement.WorkflowStepsClientSideElement"> 118 * <workflow name="content"> 119 * <steps> 120 * <step id="1"> <!-- Draft --> 121 * <workflow-actions mode="exclude"> 122 * <action>2</action><!-- Edit --> 123 * </workflow-actions> 124 * 125 * <comments mode="include"> 126 * <action>3</action><!-- propose --> 127 * </comments> 128 * </step> 129 * </steps> 130 * </workflow> 131 * </extension> 132 * 133 * Default configuration is proposed upon the StaticFileImportsClientSideElement. 134 * - default js class is "Ametys.plugins.cms.content.controller.WorkflowMenu" 135 * - default js files are loaded : /plugins/cms/resources/js/Ametys/plugins/cms/content/controller/WorkflowMenu.js and /plugins/cms/resources/js/Ametys/plugins/cms/content/actions/WorkflowAction.js 136 * - default js parameters are 137 * - label (to the i18nkey application:WORFKLOW_STEP_NAME) 138 * - description (to the i18nkey application:WORFKLOW_STEP_NAME_DESCRIPTION) 139 * - footerDescription (to the i18nkey application:WORFKLOW_STEP_NAME_FOOTER) 140 * - selection-target-id (to ^content$) 141 * - icon-small, icon-medium, icon-large (to /plugins/cms/resources_workflow/WORFKLOW_STEP_NAME-small.png for an application i18nkey, or /plugins/PLUGINNAME/resources/img/workflow/WORFKLOW_STEP_NAME-small.png for a plugin i18nkey) 142 * - workflow-step (tp the value configured avove) 143 * - workflow-name (to the value configured above) 144 * - selection-enable-multiselection (true) 145 * - selection-description-empty : i18nkey plugin.cms:CONTENT_WORKFLOW_NOSELECTION_DESCRIPTION 146 * - selection-description-nomatch : i18nkey plugin.cms:CONTENT_WORKFLOW_NOMATCH_DESCRIPTION 147 * - selection-description-multiselectionforbidden : i18nkey plugin.cms:CONTENT_WORKFLOW_NOMULTISELECT_DESCRIPTION 148 * - additionnal js parameters are 149 * - noaction-available-description : i18nkey for description when no actions are available, plugin.cms:CONTENT_WORKFLOW_NOACTIONAVAILABLE_DESCRIPTION 150 * - refreshing-description : i18nkey for description when refreshing, plugin.cms:CONTENT_WORKFLOW_REFRESH_DESCRIPTION 151 * - contentselected-start-description : i18nkey for description of the current content selection (start of the description) : plugin.cms:CONTENT_WORKFLOW_DESCRIPTION_BEGIN 152 * - contentselected-end-description : i18nkey for description of the current content selection (end of the description) : plugin.cms:CONTENT_WORKFLOW_DESCRIPTION_END 153 * - editing-description : i18nkey for description when the content is beeing edited : plugin.cms:CONTENT_WORKFLOW_EDITING_DESCRIPTION 154 * So there is no need to define a <class> by default or import js files. 155 * Additionnal i18nkeys used are plugin.cms:CONTENT_WORKFLOW_DESCRIPTION and plugin.cms:CONTENT_WORKFLOW_LOCKED_DESCRIPTION 156 */ 157public class WorkflowStepsClientSideElement extends StaticFileImportsClientSideElement implements MenuClientSideElement 158{ 159 /** The client side element component manager for menu items. */ 160 protected ThreadSafeComponentManager<ClientSideElement> _menuItemManager; 161 /** The service manager */ 162 protected ServiceManager _smanager; 163 /** Runtime users manager */ 164 protected UserManager _userManager; 165 /** Workflow provider */ 166 protected WorkflowProvider _workflowProvider; 167 /** Workflow helper */ 168 protected WorkflowHelper _workflowHelper; 169 /** Ametys object resolver */ 170 protected AmetysObjectResolver _resolver; 171 /** The content helper */ 172 protected ContentHelper _contentHelper; 173 /** The referenced client side element */ 174 protected List<ClientSideElement> _referencedClientSideElement; 175 /** The menu items */ 176 protected Map<String, List<ClientSideElement>> _menuItems; 177 /** The unresolved items */ 178 protected Map<String, List<String>> _unresolvedMenuItems; 179 /** The scripts */ 180 protected List<Script> _scripts; 181 182 @Override 183 public void service(ServiceManager manager) throws ServiceException 184 { 185 super.service(manager); 186 _smanager = manager; 187 _userManager = (UserManager) manager.lookup(UserManager.ROLE); 188 _workflowProvider = (WorkflowProvider) manager.lookup(WorkflowProvider.ROLE); 189 _workflowHelper = (WorkflowHelper) manager.lookup(WorkflowHelper.ROLE); 190 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 191 _contentHelper = (ContentHelper) manager.lookup(ContentHelper.ROLE); 192 } 193 194 @Override 195 public void configure(Configuration configuration) throws ConfigurationException 196 { 197 _menuItemManager = new ThreadSafeComponentManager<>(); 198 _menuItemManager.setLogger(LoggerFactory.getLogger("cms.plugin.threadsafecomponent")); 199 _menuItemManager.service(_smanager); 200 201 // Menu items 202 _referencedClientSideElement = new ArrayList<>(); 203 _unresolvedMenuItems = new HashMap<>(); 204 _menuItems = new HashMap<>(); 205 206 super.configure(configuration); 207 208 _configureWorkflow(configuration); 209 } 210 211 /** 212 * Read the workflow configuration, to set up the scripts. 213 * @param configuration The configuration 214 * @throws ConfigurationException If an error occurs 215 */ 216 protected void _configureWorkflow(Configuration configuration) throws ConfigurationException 217 { 218 Configuration workflowConfiguration = configuration.getChild("workflow", false); 219 if (workflowConfiguration != null) 220 { 221 String workflowName = workflowConfiguration.getAttribute("name"); 222 223 Configuration actionsConfiguration = workflowConfiguration.getChild("workflow-actions", true); 224 List<Integer> actions = new ArrayList<>(); 225 boolean actionsIncludeMode = "include".equals(actionsConfiguration.getAttribute("mode", "exclude")); 226 for (Configuration action : actionsConfiguration.getChildren("action")) 227 { 228 actions.add(action.getValueAsInteger()); 229 } 230 231 Configuration stepsConfiguration = workflowConfiguration.getChild("workflow-steps", true); 232 List<Integer> steps = new ArrayList<>(); 233 boolean stepsIncludeMode = "include".equals(stepsConfiguration.getAttribute("mode", "exclude")); 234 for (Configuration action : stepsConfiguration.getChildren("step")) 235 { 236 steps.add(action.getValueAsInteger()); 237 } 238 239 Map<Integer, Configuration> specificStepConfig = new HashMap<>(); 240 for (Configuration stepConfiguration : workflowConfiguration.getChild("steps", true).getChildren("step")) 241 { 242 Integer id = stepConfiguration.getAttributeAsInteger("id"); 243 specificStepConfig.put(id, stepConfiguration); 244 } 245 246 WorkflowDescriptor workflowDescriptor = _workflowHelper.getWorkflowDescriptor(workflowName); 247 248 if (workflowDescriptor == null) 249 { 250 throw new ConfigurationException("Unknown workflow name '" + workflowName + "' specified in configuration", workflowConfiguration); 251 } 252 253 List<Integer> allowedStepIds = _getAllowedSteps(workflowDescriptor, stepsIncludeMode, steps); 254 List<Integer> allowedActionIds = _getAllowedActions(workflowDescriptor, allowedStepIds, actionsIncludeMode, actions); 255 256 _configureScripts(workflowConfiguration, allowedStepIds, specificStepConfig, allowedActionIds, workflowDescriptor); 257 } 258 } 259 260 /** 261 * Get the steps allowed for the current workflow 262 * @param workflowDescriptor The workflow descriptor 263 * @param stepsIncludeMode True if the step listed should be the only steps included, false if they should be excluded from all available workflow's steps. 264 * @param configuredSteps A list of step ids. 265 * @return The steps allowed. 266 */ 267 protected List<Integer> _getAllowedSteps(WorkflowDescriptor workflowDescriptor, boolean stepsIncludeMode, List<Integer> configuredSteps) 268 { 269 List<Integer> workflowStepIds = workflowDescriptor.getSteps().stream() 270 .mapToInt(step -> ((StepDescriptor) step).getId()) 271 .boxed() 272 .collect(Collectors.toList()); 273 274 if (stepsIncludeMode) 275 { 276 List<Integer> stepsToRemove = new ArrayList<>(); 277 for (Integer stepId : configuredSteps) 278 { 279 if (!workflowStepIds.contains(stepId)) 280 { 281 getLogger().warn("Unknown step id '" + stepId + "' for workflow '" + workflowDescriptor.getName() + "', in workflow-steps configuration of extension '" + this.getId() + "', it will be ignored"); 282 stepsToRemove.add(stepId); 283 } 284 } 285 configuredSteps.removeAll(stepsToRemove); 286 287 return configuredSteps; 288 } 289 else 290 { 291 List<Integer> unknownStepIds = new ArrayList<>(configuredSteps); 292 unknownStepIds.removeAll(workflowStepIds); 293 for (Integer stepId : unknownStepIds) 294 { 295 getLogger().warn("Unknown step id '" + stepId + "' for workflow '" + workflowDescriptor.getName() + "', in workflow-steps configuration of extension '" + this.getId() + "', it will be ignored"); 296 } 297 298 workflowStepIds.removeAll(configuredSteps); 299 return workflowStepIds; 300 } 301 } 302 303 /** 304 * Get the actions allowed for the current workflow and only for the specified steps. 305 * @param workflowDescriptor The workflow descriptor 306 * @param stepIds The list of steps specified 307 * @param actionsIncludeMode True if the action listed should be the only actions included, false if they should be excluded from all available workflow's actions. 308 * @param configuredActions A list of actions ids. 309 * @return The actions allowed. 310 */ 311 @SuppressWarnings("unchecked") 312 protected List<Integer> _getAllowedActions(WorkflowDescriptor workflowDescriptor, List<Integer> stepIds, boolean actionsIncludeMode, List<Integer> configuredActions) 313 { 314 if (actionsIncludeMode) 315 { 316 return configuredActions; 317 } 318 else 319 { 320 // get the list of action ids for the allowed steps. 321 List<Integer> workflowActionIds = stepIds.stream() 322 .map(stepId -> workflowDescriptor.getStep(stepId).getActions()) 323 .flatMap(actionsLists -> ((List<ActionDescriptor>) actionsLists).stream()) 324 .distinct() 325 .mapToInt(ActionDescriptor::getId) 326 .boxed() 327 .collect(Collectors.toList()); 328 329 workflowActionIds.removeAll(configuredActions); 330 return workflowActionIds; 331 } 332 } 333 334 /** 335 * Configure the list of Scripts, for each step available to the workflow. 336 * @param workflowConfiguration The configuration 337 * @param stepIds The list of steps 338 * @param stepsConfiguration The parameters for each step 339 * @param allowedActionIds The list of globally allowed actions for this workflow 340 * @param workflowDescriptor The descriptor for the current workflow 341 * @throws ConfigurationException If an error occurs 342 */ 343 protected void _configureScripts(Configuration workflowConfiguration, List<Integer> stepIds, Map<Integer, Configuration> stepsConfiguration, List<Integer> allowedActionIds, WorkflowDescriptor workflowDescriptor) throws ConfigurationException 344 { 345 _scripts = new ArrayList<>(); 346 347 Map<String, Object> defaultParameters = null; 348 String defaultClassName = ""; 349 350 Configuration actionConfiguration = workflowConfiguration.getChild("action", true); 351 defaultClassName = actionConfiguration.getAttribute("name", _getDefaultMenuClassName()); 352 defaultParameters = ConfigurationHelper.parsePluginParameters(actionConfiguration, getPluginName(), getLogger()); 353 354 for (Integer stepId : stepIds) 355 { 356 Map<String, Object> parameters; 357 String className; 358 359 Configuration stepConfiguration = null; 360 361 if (stepsConfiguration.containsKey(stepId)) 362 { 363 stepConfiguration = stepsConfiguration.get(stepId); 364 parameters = ConfigurationHelper.parsePluginParameters(stepConfiguration, getPluginName(), getLogger()); 365 className = stepConfiguration.getAttribute("name", defaultClassName); 366 } 367 else 368 { 369 parameters = new HashMap<>(); 370 parameters.putAll(defaultParameters); 371 className = defaultClassName; 372 } 373 374 _configureWorkflowStep(workflowDescriptor, stepId, parameters, stepConfiguration, allowedActionIds); 375 _configureParameters(parameters); 376 377 List<ScriptFile> scriptFiles = new ArrayList<>(_script.getScriptFiles()); 378 List<ScriptFile> cssFiles = new ArrayList<>(_script.getCSSFiles()); 379 Script script = new Script(this.getId() + "-" + stepId, this.getId(), className, scriptFiles, cssFiles, parameters); 380 381 _scripts.add(script); 382 383 if (stepConfiguration != null && stepConfiguration.getChild("action", false) != null) 384 { 385 _configureMenuItems(script, stepConfiguration.getChild("action", false)); 386 } 387 else 388 { 389 _configureMenuItems(script, actionConfiguration); 390 } 391 } 392 } 393 394 /** 395 * Get the default class name for workflow menu 396 * @return the default class name 397 */ 398 protected String _getDefaultMenuClassName() 399 { 400 return "Ametys.plugins.cms.content.controller.WorkflowMenu"; 401 } 402 403 /** 404 * Get the default class name for workflow action 405 * @return the default class name 406 */ 407 protected String _getDefaultActionClassName() 408 { 409 return "Ametys.plugins.cms.content.actions.WorkflowAction.doAction"; 410 } 411 412 /** 413 * Configure the parameters specific to the workflow, for the given step 414 * @param workflowDescriptor The descriptor of the workflow 415 * @param stepId The step 416 * @param stepParameters The parameters of the step 417 * @param stepConfiguration The step configuration 418 * @param allowedActionIds The list of globally allowed actions 419 * @throws ConfigurationException If an error occurs 420 */ 421 protected void _configureWorkflowStep(WorkflowDescriptor workflowDescriptor, Integer stepId, Map<String, Object> stepParameters, Configuration stepConfiguration, List<Integer> allowedActionIds) throws ConfigurationException 422 { 423 stepParameters.put("workflow-name", workflowDescriptor.getName()); 424 stepParameters.put("workflow-step", stepId); 425 stepParameters.put("workflow-step-name", _workflowHelper.getStepName(workflowDescriptor.getName(), stepId)); 426 427 List<Integer> currentStepActions = workflowDescriptor.getStep(stepId) 428 .getActions() 429 .stream() 430 .mapToInt(action -> ((ActionDescriptor) action).getId()) 431 .boxed() 432 .collect(Collectors.toList()); 433 List<Integer> allowedStepActions = ListUtils.intersection(currentStepActions, allowedActionIds); 434 435 List<Integer> stepActions = new ArrayList<>(); 436 List<Integer> commentIds = new ArrayList<>(); 437 438 if (stepConfiguration != null) 439 { 440 Configuration stepWorkflowActions = stepConfiguration.getChild("workflow-actions", false); 441 if (stepWorkflowActions != null) 442 { 443 stepActions.addAll(_configureWorkflowStepActions(workflowDescriptor.getName(), stepId, currentStepActions, allowedStepActions, stepWorkflowActions)); 444 } 445 else 446 { 447 stepActions.addAll(allowedStepActions); 448 } 449 450 Configuration stepWorkflowComments = stepConfiguration.getChild("comments", false); 451 if (stepWorkflowComments != null) 452 { 453 commentIds.addAll(_configureWorkflowStepActions(workflowDescriptor.getName(), stepId, currentStepActions, allowedStepActions, stepWorkflowComments)); 454 } 455 else 456 { 457 commentIds.addAll(allowedStepActions); 458 } 459 } 460 else 461 { 462 stepActions.addAll(allowedStepActions); 463 commentIds.addAll(allowedStepActions); 464 } 465 466 stepParameters.put("workflow-actions-ids", stepActions); 467 stepParameters.put("workflow-comments-ids", commentIds); 468 } 469 470 /** 471 * Get the list of actions available for a step, from the configuration 472 * @param workflowName The name of the current workflow 473 * @param stepId The step 474 * @param currentStepActions All the actions available for this step 475 * @param allowedStepActions The actions allowed by the configuration for this step 476 * @param stepActionsConfiguration The configuration for the step actions 477 * @return The list of actions for the step 478 * @throws ConfigurationException If an error occurs 479 */ 480 protected List<Integer> _configureWorkflowStepActions(String workflowName, Integer stepId, List<Integer> currentStepActions, List<Integer> allowedStepActions, Configuration stepActionsConfiguration) throws ConfigurationException 481 { 482 List<Integer> stepActions = new ArrayList<>(); 483 484 boolean actionsIncludeMode = "include".equals(stepActionsConfiguration.getAttribute("mode", "exclude")); 485 if (actionsIncludeMode) 486 { 487 // include mode, add all listed action if they are available for the current step. 488 for (Configuration workflowAction : stepActionsConfiguration.getChildren("action")) 489 { 490 int action = workflowAction.getValueAsInteger(); 491 if (currentStepActions.contains(action)) 492 { 493 stepActions.add(action); 494 } 495 else 496 { 497 getLogger().warn("Unknown action id '" + action + "' for step '" + stepId + "', for workflow '" 498 + workflowName + "', in steps configuration of extension '" + this.getId() + "', it will be ignored"); 499 } 500 } 501 } 502 else 503 { 504 // exclude mode, add all allowed step actions but the ones listed 505 stepActions.addAll(allowedStepActions); 506 for (Configuration workflowAction : stepActionsConfiguration.getChildren("action")) 507 { 508 Integer action = workflowAction.getValueAsInteger(); 509 if (allowedStepActions.contains(action)) 510 { 511 stepActions.remove(action); 512 } 513 else 514 { 515 getLogger().warn("Unknown action id '" + action + "' for step '" + stepId + "', for workflow '" 516 + workflowName + "', in steps configuration of extension '" + this.getId() + "', it will be ignored"); 517 } 518 } 519 } 520 521 return stepActions; 522 } 523 524 @Override 525 public List<ClientSideElement> getReferencedClientSideElements(Map<String, Object> contextParameters) 526 { 527 if (hasRight(getRights(contextParameters))) 528 { 529 return _referencedClientSideElement; 530 } 531 else 532 { 533 return List.of(); 534 } 535 } 536 537 @Override 538 public List<Script> getScripts(boolean ignoreRights, Map<String, Object> contextParameters) 539 { 540 try 541 { 542 _resolveMenuItems(); 543 } 544 catch (Exception e) 545 { 546 throw new IllegalStateException("Unable to lookup client side element local components", e); 547 } 548 549 for (Script script : _scripts) 550 { 551 Map<String, Object> parameters = script.getParameters(); 552 553 if (_menuItems.containsKey(script.getId())) 554 { 555 List<String> menuItems = new ArrayList<>(); 556 for (ClientSideElement element : _menuItems.get(script.getId())) 557 { 558 menuItems.add(element.getId()); 559 } 560 parameters.put("menu-items", menuItems); 561 } 562 } 563 564 return _scripts; 565 } 566 567 @Override 568 protected Script _configureScript(Configuration configuration) throws ConfigurationException 569 { 570 Script script = super._configureScript(configuration); 571 List<ScriptFile> scriptFiles = new ArrayList<>(); 572 scriptFiles.add(new ScriptFile("/plugins/cms/resources/js/Ametys/plugins/cms/content/controller/WorkflowMenu.js")); 573 scriptFiles.add(new ScriptFile("/plugins/cms/resources/js/Ametys/plugins/cms/content/actions/WorkflowAction.js")); 574 script.getScriptFiles().addAll(scriptFiles); 575 576 return script; 577 } 578 579 /** 580 * Configure parameters recursively 581 * @param parameters The parameters map to fill 582 * @throws ConfigurationException The configuration is incorrect 583 */ 584 protected void _configureParameters(Map<String, Object> parameters) throws ConfigurationException 585 { 586 String workflowStepName = (String) parameters.get("workflow-step-name"); 587 588 // Default label 589 if (parameters.get("label") == null) 590 { 591 parameters.put("label", new I18nizableText("application", workflowStepName)); 592 } 593 594 // Default description 595 if (parameters.get("description") == null) 596 { 597 parameters.put("description", new I18nizableText("application", workflowStepName + "_DESCRIPTION")); 598 } 599 600 // Default footer description 601 if (parameters.get("footerDescription") == null) 602 { 603 parameters.put("footerDescription", new I18nizableText("application", workflowStepName + "_FOOTER")); 604 } 605 606 // Selection target type 607 if (parameters.get("selection-target-id") == null) 608 { 609 parameters.put("selection-target-id", _getSelectionTargetId()); 610 // DO NOT add selection-target-parameter with workflow name : the button is show/hide according the workflow name on client side 611 } 612 613 // Default icons 614 String[] icons = new String[]{"-small", "-medium", "-large"}; 615 for (String icon : icons) 616 { 617 if (parameters.get("icon" + icon) == null) 618 { 619 I18nizableText workflowStepNameText = new I18nizableText("application", workflowStepName); 620 if ("application".equals(workflowStepNameText.getCatalogue())) 621 { 622 parameters.put("icon" + icon, new I18nizableText("/plugins/" + _getDefaultPluginName() + "/resources_workflow/" + workflowStepNameText.getKey() + icon + ".png")); 623 } 624 else 625 { 626 String pluginName = workflowStepNameText.getCatalogue().substring("plugin.".length()); 627 parameters.put("icon" + icon, new I18nizableText("/plugins/" + pluginName + "/resources/img/workflow/" + workflowStepNameText.getKey() + icon + ".png")); 628 } 629 } 630 } 631 632 // Multiselection enabled 633 if (!parameters.containsKey("selection-enable-multiselection")) 634 { 635 parameters.put("selection-enable-multiselection", true); 636 } 637 638 _configureDefaultDescriptions(parameters); 639 } 640 641 /** 642 * Get the default plugin name 643 * @return the default plugin name 644 */ 645 protected String _getDefaultPluginName() 646 { 647 return "cms"; 648 } 649 650 /** 651 * Get the selection target id (can be a Regexp) 652 * @return the selection target id 653 */ 654 protected String _getSelectionTargetId() 655 { 656 return "^content$"; 657 } 658 659 /** 660 * Configure the menu items 661 * @param script The parameters map to fill 662 * @param configuration The items configuration 663 */ 664 @SuppressWarnings("unchecked") 665 protected void _configureMenuItems(Script script, Configuration configuration) 666 { 667 Map<String, Object> parameters = script.getParameters(); 668 for (int actionId : (List<Integer>) parameters.get("workflow-actions-ids")) 669 { 670 String id = script.getId() + ".workflow-action-" + actionId; 671 672 String actionName = _workflowHelper.getActionName((String) parameters.get("workflow-name"), actionId); 673 674 DefaultConfiguration conf = new DefaultConfiguration("extension"); 675 conf.setAttribute("id", id); 676 677 DefaultConfiguration classConf = new DefaultConfiguration("class"); 678 classConf.setAttribute("name", "Ametys.ribbon.element.ui.ButtonController"); 679 680 // Label 681 _addActionLabelConfiguration(configuration, classConf, actionId, actionName); 682 683 // Description 684 _addActionDescriptionConfiguration(configuration, classConf, actionId, actionName); 685 686 // Workflow action id 687 DefaultConfiguration wActionConf = new DefaultConfiguration("workflow-action-id"); 688 wActionConf.setValue(actionId); 689 classConf.addChild(wActionConf); 690 691 // Action 692 DefaultConfiguration actionConf = new DefaultConfiguration("action"); 693 actionConf.setValue(configuration.getChild("menu-" + actionId + "-action").getValue(_getDefaultActionClassName())); 694 classConf.addChild(actionConf); 695 696 // Comment 697 _addActionCommentConfiguration(configuration, classConf, actionId, actionName, parameters); 698 699 _additionalMenuItemConfiguration(configuration, classConf, actionId, parameters); 700 701 conf.addChild(classConf); 702 703 _menuItemManager.addComponent(_pluginName, null, id, StaticClientSideElement.class, conf); 704 if (!_unresolvedMenuItems.containsKey(script.getId())) 705 { 706 _unresolvedMenuItems.put(script.getId(), new ArrayList<>()); 707 } 708 _unresolvedMenuItems.get(script.getId()).add(id); 709 } 710 } 711 712 /** 713 * Add the action label configuration 714 * @param configuration the parent configuration 715 * @param classConf the class configuration 716 * @param actionId the action id 717 * @param actionName the action name 718 */ 719 protected void _addActionLabelConfiguration(Configuration configuration, DefaultConfiguration classConf, int actionId, String actionName) 720 { 721 DefaultConfiguration labelConf = new DefaultConfiguration("label"); 722 labelConf.setAttribute("i18n", configuration.getChild("menu-" + actionId + "-label").getAttribute("i18n", "true")); 723 labelConf.setValue(configuration.getChild("menu-" + actionId + "-label").getValue(actionName)); 724 classConf.addChild(labelConf); 725 } 726 727 /** 728 * Add the action description configuration 729 * @param configuration the parent configuration 730 * @param classConf the class configuration 731 * @param actionId the action id 732 * @param actionName the action name 733 */ 734 protected void _addActionDescriptionConfiguration(Configuration configuration, DefaultConfiguration classConf, int actionId, String actionName) 735 { 736 DefaultConfiguration descConf = new DefaultConfiguration("description"); 737 descConf.setAttribute("i18n", configuration.getChild("menu-" + actionId + "-description").getAttribute("i18n", "true")); 738 descConf.setValue(configuration.getChild("menu-" + actionId + "-description").getValue(actionName + "_DESCRIPTION")); 739 classConf.addChild(descConf); 740 } 741 742 /** 743 * Add the action comment configuration 744 * @param configuration the parent configuration 745 * @param classConf the class configuration 746 * @param actionId the action id 747 * @param actionName the action name 748 * @param parameters the script parameters 749 */ 750 @SuppressWarnings("unchecked") 751 protected void _addActionCommentConfiguration(Configuration configuration, DefaultConfiguration classConf, int actionId, String actionName, Map<String, Object> parameters) 752 { 753 DefaultConfiguration commentConf = new DefaultConfiguration("comment"); 754 commentConf.setValue(configuration.getChild("menu-" + actionId + "-comment").getValueAsBoolean(((List<Integer>) parameters.get("workflow-comments-ids")).contains(actionId))); 755 classConf.addChild(commentConf); 756 } 757 758 /** 759 * Additional configuration for menu items 760 * @param itemConf The item configuration 761 * @param classConf The class configuration 762 * @param actionId The workflow action id 763 * @param parameters The script parameters 764 */ 765 protected void _additionalMenuItemConfiguration (Configuration itemConf, DefaultConfiguration classConf, int actionId, Map<String, Object> parameters) 766 { 767 // Nothing to do 768 } 769 770 /** 771 * Configure the default description 772 * @param parameters The parameters 773 */ 774 protected void _configureDefaultDescriptions(Map<String, Object> parameters) 775 { 776 // Empty selection 777 if (!parameters.containsKey("selection-description-empty")) 778 { 779 parameters.put("selection-description-empty", new I18nizableText("plugin.cms", "CONTENT_WORKFLOW_NOSELECTION_DESCRIPTION")); 780 } 781 782 // No match selection 783 if (!parameters.containsKey("selection-description-nomatch")) 784 { 785 parameters.put("selection-description-nomatch", new I18nizableText("plugin.cms", "CONTENT_WORKFLOW_NOMATCH_DESCRIPTION")); 786 } 787 788 // Multiselection forbidden 789 if (!parameters.containsKey("selection-description-multiselectionforbidden")) 790 { 791 parameters.put("selection-description-multiselectionforbidden", new I18nizableText("plugin.cms", "CONTENT_WORKFLOW_NOMULTISELECT_DESCRIPTION")); 792 } 793 794 // No available action 795 if (!parameters.containsKey("noaction-available-description")) 796 { 797 parameters.put("noaction-available-description", new I18nizableText("plugin.cms", "CONTENT_WORKFLOW_NOACTIONAVAILABLE_DESCRIPTION")); 798 } 799 800 // Refreshing 801 if (!parameters.containsKey("refreshing-description")) 802 { 803 parameters.put("refreshing-description", new I18nizableText("plugin.cms", "CONTENT_WORKFLOW_REFRESH_DESCRIPTION")); 804 } 805 806 // Content selected 807 if (!parameters.containsKey("contentselected-start-description")) 808 { 809 parameters.put("contentselected-start-description", new I18nizableText("plugin.cms", "CONTENT_WORKFLOW_DESCRIPTION_BEGIN")); 810 } 811 if (!parameters.containsKey("contentselected-end-description")) 812 { 813 parameters.put("contentselected-end-description", new I18nizableText("plugin.cms", "CONTENT_WORKFLOW_DESCRIPTION_END")); 814 } 815 816 // Editing 817 if (!parameters.containsKey("editing-description")) 818 { 819 parameters.put("editing-description", new I18nizableText("plugin.cms", "CONTENT_WORKFLOW_EDITING_DESCRIPTION")); 820 } 821 } 822 823 /** 824 * Get the workflow state of contents 825 * @param contentsId The ids of contents to test workflow status 826 * @param scriptId The script id 827 * @return The workflow state 828 */ 829 @Callable 830 public Map<String, Object> getWorkflowState(List<String> contentsId, String scriptId) 831 { 832 List<Map<String, Object>> contents = new ArrayList<>(); 833 boolean invalidWorkflowForAllContents = true; 834 835 for (String contentId : contentsId) 836 { 837 Content content = _resolver.resolveById(contentId); 838 839 if (content instanceof WorkflowAwareContent) 840 { 841 WorkflowAwareContent waContent = (WorkflowAwareContent) content; 842 AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(waContent); 843 844 for (Script script : _scripts) 845 { 846 if (script.getId().equals(scriptId)) 847 { 848 long wId = waContent.getWorkflowId(); 849 String workflowName = (String) script.getParameters().get("workflow-name"); 850 if (!workflow.getWorkflowName(wId).equals(workflowName)) 851 { 852 continue; 853 } 854 855 invalidWorkflowForAllContents = false; 856 857 List<Step> steps = workflow.getCurrentSteps(wId); 858 List<Integer> stepIds = new ArrayList<>(); 859 for (Step step : steps) 860 { 861 stepIds.add(step.getStepId()); 862 } 863 864 Integer workflowStepId = (Integer) script.getParameters().get("workflow-step"); 865 if (stepIds.contains(workflowStepId)) 866 { 867 Map<String, Object> contentParams = _getContentParameters(content, workflow, script, wId); 868 contents.add(contentParams); 869 } 870 } 871 } 872 } 873 } 874 875 Map<String, Object> results = new HashMap<>(); 876 results.put("contents", contents); 877 878 if (invalidWorkflowForAllContents) 879 { 880 results.put("invalidWorkflow", true); 881 } 882 return results; 883 } 884 885 @SuppressWarnings("unchecked") 886 private Map<String, Object> _getContentParameters(Content content, AmetysObjectWorkflow workflow, Script script, long wId) 887 { 888 Map<String, Object> contentParams = new HashMap<>(); 889 contentParams.put("id", content.getId()); 890 contentParams.put("title", _contentHelper.getTitle(content)); 891 892 String i18nKey = "CONTENT_WORKFLOW_DESCRIPTION"; 893 894 List<String> workflowI18nParameters = new ArrayList<>(); 895 workflowI18nParameters.add(_contentHelper.getTitle(content)); 896 897 boolean isLocked = false; 898 if (content instanceof LockableAmetysObject) 899 { 900 LockableAmetysObject lockableContent = (LockableAmetysObject) content; 901 if (lockableContent.isLocked() && !LockHelper.isLockOwner(lockableContent, _currentUserProvider.getUser())) 902 { 903 i18nKey = "CONTENT_WORKFLOW_LOCKED_DESCRIPTION"; 904 905 UserIdentity currentLockerIdentity = lockableContent.getLockOwner(); 906 User currentLocker = currentLockerIdentity != null ? _userManager.getUser(currentLockerIdentity.getPopulationId(), currentLockerIdentity.getLogin()) : null; 907 908 workflowI18nParameters.add(currentLocker != null ? currentLocker.getFullName() : ""); 909 workflowI18nParameters.add(currentLockerIdentity != null ? currentLockerIdentity.getLogin() : "Anonymous"); 910 911 isLocked = true; 912 } 913 } 914 915 contentParams.put("description", new I18nizableText("plugin.cms", i18nKey, workflowI18nParameters)); 916 contentParams.put("locked", isLocked); 917 918 Map<String, Object> vars = new HashMap<>(); 919 vars.put(AbstractContentWorkflowComponent.CONTENT_KEY, content); 920 vars.put(AbstractWorkflowComponent.FAIL_CONDITIONS_KEY, new ArrayList<> ()); 921 922 int[] availableActions = workflow.getAvailableActions(wId, vars); 923 Arrays.sort(availableActions); 924 925 List<Integer> activeActions = new ArrayList<>(); 926 for (int actionId : (List<Integer>) script.getParameters().get("workflow-actions-ids")) 927 { 928 if (Arrays.binarySearch(availableActions, actionId) >= 0) 929 { 930 activeActions.add(actionId); 931 } 932 } 933 contentParams.put("actions", activeActions); 934 935 return contentParams; 936 } 937 938 private void _resolveMenuItems() throws Exception 939 { 940 if (_unresolvedMenuItems != null) 941 { 942 _menuItemManager.initialize(); 943 944 for (String scriptId : _unresolvedMenuItems.keySet()) 945 { 946 for (String id : _unresolvedMenuItems.get(scriptId)) 947 { 948 ClientSideElement element; 949 try 950 { 951 element = _menuItemManager.lookup(id); 952 } 953 catch (ComponentException e) 954 { 955 throw new Exception("Unable to lookup client side element role: '" + id + "'", e); 956 } 957 958 if (!_menuItems.containsKey(scriptId)) 959 { 960 _menuItems.put(scriptId, new ArrayList<>()); 961 } 962 _menuItems.get(scriptId).add(element); 963 _referencedClientSideElement.add(element); 964 } 965 } 966 } 967 968 _unresolvedMenuItems = null; 969 } 970}