[#68734] Migrate webhooks form jQuery to Stimulus

Replaces jQuery-based enable/disable logic in the webhooks admin form
with a new Stimulus controller `disable-when-value-selected`.

This change was originally deferred from PR #20884 as it was planned to
be addressed in Work Package #69436 / PR #21227. Since that work has
been put on hold and jQuery removal is progressing, this commit
completes the migration now.

Changes:
- Add DisableWhenValueSelectedController for declarative form toggling
- Add toggleEnabled/enableElement/disableElement helpers to dom-helpers
- Remove jQuery code from webhooks admin form
- Apply Stimulus controller to project selection fieldset

Related to opf/openproject#20884

https://community.openproject.org/wp/68734
This commit is contained in:
Alexander Brandon Coles
2026-05-25 15:13:12 +00:00
parent b49d3c8c44
commit 6a86c267f4
4 changed files with 120 additions and 17 deletions
@@ -243,3 +243,35 @@ export function attributeTokenList(element:HTMLElement, attribute:string):DOMTok
return list as DOMTokenList;
}
/* eslint-enable */
/**
* Toggles the enabled/disabled state of form elements.
* For fieldsets, recursively applies to all child elements.
*
* @param element the element to toggle
* @param value force state (optional): `true` to enable/`false` to disable
*/
export function toggleEnabled(element:HTMLElement, value?:boolean) {
if (
element instanceof HTMLInputElement ||
element instanceof HTMLSelectElement ||
element instanceof HTMLTextAreaElement ||
element instanceof HTMLButtonElement ||
element instanceof HTMLFieldSetElement
) {
if (typeof value === 'undefined') {
element.disabled = !element.disabled;
} else {
element.disabled = !value;
}
}
if (element instanceof HTMLFieldSetElement) {
Array.from(element.elements).forEach((child) => {
toggleEnabled(child as HTMLElement, !element.disabled);
});
}
}
export const enableElement = (element:HTMLElement) => toggleEnabled(element, true);
export const disableElement = (element:HTMLElement) => toggleEnabled(element, false);
@@ -0,0 +1,69 @@
/*
* -- copyright
* OpenProject is an open source project management software.
* Copyright (C) the OpenProject GmbH
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License version 3.
*
* OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
* Copyright (C) 2006-2013 Jean-Philippe Lang
* Copyright (C) 2010-2013 the ChiliProject Team
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* See COPYRIGHT and LICENSE files for more details.
* ++
*/
import { toggleEnabled } from 'core-app/shared/helpers/dom-helpers';
import { ApplicationController } from 'stimulus-use';
export default class OpDisableWhenValueSelectedController extends ApplicationController {
static targets = ['cause', 'effect'];
declare readonly effectTargets:(HTMLInputElement|HTMLFieldSetElement)[];
private boundListener = this.toggleDisabled.bind(this);
causeTargetConnected(target:HTMLElement) {
target.addEventListener('change', this.boundListener);
}
causeTargetDisconnected(target:HTMLElement) {
target.removeEventListener('change', this.boundListener);
}
private toggleDisabled(evt:Event):void {
const input = evt.target as HTMLInputElement;
const targetName = input.dataset.targetName;
this
.effectTargets
.filter((el) => targetName === el.dataset.targetName)
.forEach((el) => {
const disabled = this.willDisable(el, input.value);
toggleEnabled(el, !disabled);
});
}
private willDisable(el:HTMLElement, value:string):boolean {
if (el.dataset.notValue) {
return el.dataset.notValue === value;
}
return !(el.dataset.value === value);
}
}
+2
View File
@@ -2,6 +2,7 @@ import { environment } from '../environments/environment';
import { OpApplicationController } from './controllers/op-application.controller';
import MainMenuController from './controllers/dynamic/menus/main.controller';
import OpDisableWhenCheckedController from './controllers/disable-when-checked.controller';
import OpDisableWhenValueSelectedController from './controllers/disable-when-value-selected.controller';
import PrintController from './controllers/print.controller';
import RefreshOnFormChangesController from './controllers/refresh-on-form-changes.controller';
import FormPreviewController from './controllers/form-preview.controller';
@@ -57,6 +58,7 @@ OpenProjectStimulusApplication.preregister('application', OpApplicationControlle
OpenProjectStimulusApplication.preregister('async-dialog', AsyncDialogController);
OpenProjectStimulusApplication.preregister('disable-when-checked', OpDisableWhenCheckedController);
OpenProjectStimulusApplication.preregister('disable-when-clicked', DisableWhenClickedController);
OpenProjectStimulusApplication.preregister('disable-when-value-selected', OpDisableWhenValueSelectedController);
OpenProjectStimulusApplication.preregister('flash', FlashController);
OpenProjectStimulusApplication.preregister('menus--main', MainMenuController);
OpenProjectStimulusApplication.preregister('require-password-confirmation', RequirePasswordConfirmationController);
@@ -70,7 +70,7 @@
<% end %>
</fieldset>
<fieldset class="form--fieldset">
<fieldset class="form--fieldset" data-controller="disable-when-value-selected">
<legend class="form--fieldset-legend">
<%= t "webhooks.outgoing.form.project_ids.title" %>
</legend>
@@ -80,14 +80,22 @@
"all",
checked: @webhook.all_projects?,
label: t("webhooks.outgoing.form.project_ids.all"),
container_class: "-wide" %>
container_class: "-wide",
data: {
disable_when_value_selected_target: "cause",
target_name: "webhook_project_ids"
} %>
</div>
<div class="form--field">
<%= f.radio_button :project_ids,
"selection",
checked: !@webhook.all_projects?,
label: t("webhooks.outgoing.form.project_ids.selected"),
container_class: "-wide" %>
container_class: "-wide",
data: {
disable_when_value_selected_target: "cause",
target_name: "webhook_project_ids"
} %>
</div>
<div class="form--field">
@@ -99,23 +107,15 @@
id,
!@webhook.all_projects? && @webhook.project_ids.include?(id),
disabled: @webhook.all_projects?,
class: "webhooks--selected-project-ids" -%>
class: "webhooks--selected-project-ids",
data: {
disable_when_value_selected_target: "effect",
target_name: "webhook_project_ids",
value: "selection"
} -%>
<%= name %>
</label>
<% end %>
</div>
</div>
</fieldset>
<%= nonced_javascript_tag do %>
(function($) {
// Toggle selector for new/edit webhooks projects
$('input[name="webhook[project_ids]"]').change(function(){
$('.webhooks--selected-project-ids').prop('disabled', $(this).val() === 'all');
});
$('input[name="webhook[type_ids]"]').change(function(){
$('.webhooks--selected-type-ids').prop('disabled', $(this).val() === 'all');
});
}(jQuery));
<% end %>