diff --git a/ts/WoltLabSuite/Core/Acp/Form/Builder/Field/Devtools/Project/Instructions.ts b/ts/WoltLabSuite/Core/Acp/Form/Builder/Field/Devtools/Project/Instructions.ts
index 9a773afc354..5dfabfcbaa8 100644
--- a/ts/WoltLabSuite/Core/Acp/Form/Builder/Field/Devtools/Project/Instructions.ts
+++ b/ts/WoltLabSuite/Core/Acp/Form/Builder/Field/Devtools/Project/Instructions.ts
@@ -126,8 +126,8 @@ class Instructions {
event.preventDefault();
event.stopPropagation();
- const instructionsId = ((event.currentTarget as HTMLElement).closest("li.section") as HTMLElement).dataset
- .instructionsId!;
+ const section = (event.currentTarget as HTMLElement).closest("li.section") as HTMLElement;
+ const instructionsId = section.dataset.instructionsId!;
// note: data will be validated/filtered by the server
@@ -140,6 +140,10 @@ class Instructions {
return;
}
+ if (!this.validateVoidUsage(pipField, section, pipField.value, null)) {
+ return;
+ }
+
const valueField = document.getElementById(
`${this.formFieldId}_instructions${instructionsId}_value`,
) as HTMLInputElement;
@@ -367,8 +371,64 @@ class Instructions {
// toggle application selector
this.toggleApplicationFormField(instructionsId);
- const value = document.getElementById(`${this.formFieldId}_instructions${instructionsId}_value`)!;
- value.focus();
+ // hide value/runStandalone fields when the void instruction is selected
+ this.toggleValueAndRunStandaloneFormFields(instructionsId, pip !== "void");
+
+ if (pip !== "void") {
+ const value = document.getElementById(`${this.formFieldId}_instructions${instructionsId}_value`)!;
+ value.focus();
+ }
+ }
+
+ /**
+ * Toggles the visibility of the value and runStandalone form fields based on the selected pip.
+ */
+ protected toggleValueAndRunStandaloneFormFields(instructionsId: InstructionsId, show: boolean): void {
+ const valueDl = document.getElementById(`${this.formFieldId}_instructions${instructionsId}_value`)!.closest("dl")!;
+ const runStandaloneDl = document
+ .getElementById(`${this.formFieldId}_instructions${instructionsId}_runStandalone`)!
+ .closest("dl")!;
+
+ if (show) {
+ DomUtil.show(valueDl);
+ DomUtil.show(runStandaloneDl);
+ } else {
+ DomUtil.hide(valueDl);
+ DomUtil.hide(runStandaloneDl);
+ }
+ }
+
+ /**
+ * Validates that the `void` instruction is only used inside `update` sections and that it is
+ * the only instruction within its section. Returns `false` and shows an inline error if the
+ * placement is invalid.
+ */
+ protected validateVoidUsage(
+ errorTarget: HTMLElement,
+ section: HTMLElement,
+ pip: string,
+ excludedInstructionId: string | null,
+ ): boolean {
+ const instructionList = section.querySelector(".sortableList") as HTMLElement;
+ const existingInstructions = Array.from(instructionList.children) as HTMLElement[];
+ const otherInstructions = existingInstructions.filter((li) => li.dataset.instructionId !== excludedInstructionId);
+
+ if (pip === "void") {
+ if (section.dataset.type !== "update") {
+ DomUtil.innerError(errorTarget, Language.get("wcf.acp.devtools.project.instruction.error.voidInInstall"), true);
+ return false;
+ }
+ if (otherInstructions.length > 0) {
+ DomUtil.innerError(errorTarget, Language.get("wcf.acp.devtools.project.instruction.error.voidNotAlone"), true);
+ return false;
+ }
+ } else if (otherInstructions.some((li) => li.dataset.pip === "void")) {
+ DomUtil.innerError(errorTarget, Language.get("wcf.acp.devtools.project.instruction.error.voidNotAlone"), true);
+ return false;
+ }
+
+ DomUtil.innerError(errorTarget, "");
+ return true;
}
/**
@@ -403,6 +463,11 @@ class Instructions {
const submit = () => {
const listItem = document.getElementById(`${this.formFieldId}_instruction${instructionId}`)!;
+ const section = listItem.closest("li.section") as HTMLElement;
+ if (!this.validateVoidUsage(pipSelect, section, pipSelect.value, instructionId)) {
+ return;
+ }
+
listItem.dataset.application =
Instructions.applicationPips.indexOf(pipSelect.value) !== -1 ? applicationSelect.value : "";
listItem.dataset.pip = pipSelect.value;
@@ -443,6 +508,14 @@ class Instructions {
DomUtil.hide(applicationSelect.closest("dl")!);
}
+ if (pip === "void") {
+ DomUtil.hide(valueInput.closest("dl")!);
+ DomUtil.hide(runStandaloneInput.closest("dl")!);
+ } else {
+ DomUtil.show(valueInput.closest("dl")!);
+ DomUtil.show(runStandaloneInput.closest("dl")!);
+ }
+
const description = DomTraverse.nextByTag(valueInput, "SMALL")!;
if (this.pipDefaultFilenames[pip] !== "") {
description.innerHTML = Language.get(
diff --git a/wcfsetup/install/files/acp/templates/__devtoolsProjectInstructionsFormField.tpl b/wcfsetup/install/files/acp/templates/__devtoolsProjectInstructionsFormField.tpl
index d8302879fc0..fecd96763c5 100644
--- a/wcfsetup/install/files/acp/templates/__devtoolsProjectInstructionsFormField.tpl
+++ b/wcfsetup/install/files/acp/templates/__devtoolsProjectInstructionsFormField.tpl
@@ -64,6 +64,7 @@
{foreach from=$packageInstallationPlugins item=packageInstallationPlugin}
{/foreach}
+
@@ -131,6 +132,7 @@
{foreach from=$packageInstallationPlugins item=packageInstallationPlugin}
{/foreach}
+
@@ -188,6 +190,8 @@
) {
Language.addObject({
'wcf.acp.devtools.project.instruction.delete.confirmMessages': '{jslang}wcf.acp.devtools.project.instruction.delete.confirmMessages{/jslang}',
+ 'wcf.acp.devtools.project.instruction.error.voidInInstall': '{jslang}wcf.acp.devtools.project.instruction.error.voidInInstall{/jslang}',
+ 'wcf.acp.devtools.project.instruction.error.voidNotAlone': '{jslang}wcf.acp.devtools.project.instruction.error.voidNotAlone{/jslang}',
'wcf.acp.devtools.project.instruction.edit': '{jslang}wcf.acp.devtools.project.instruction.edit{/jslang}',
'wcf.acp.devtools.project.instruction.instruction': '{jslang __literal=true}wcf.acp.devtools.project.instruction.instruction{/jslang}',
'wcf.acp.devtools.project.instruction.value.description': '{jslang}wcf.acp.devtools.project.instruction.value.description{/jslang}',
@@ -214,6 +218,7 @@
{implode from=$packageInstallationPlugins item=packageInstallationPlugin}
'{$packageInstallationPlugin->pluginName}': '{$packageInstallationPlugin->getDefaultFilename()}'
{/implode}
+ , 'void': ''
},
[
{implode from=$field->getValue() key=instructionsKey item=instructions}
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Form/Builder/Field/Devtools/Project/Instructions.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Form/Builder/Field/Devtools/Project/Instructions.js
index 900b7418e10..e076a99a52d 100644
--- a/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Form/Builder/Field/Devtools/Project/Instructions.js
+++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Form/Builder/Field/Devtools/Project/Instructions.js
@@ -82,14 +82,17 @@ define(["require", "exports", "tslib", "../../../../../../Core", "../../../../..
addInstruction(event) {
event.preventDefault();
event.stopPropagation();
- const instructionsId = event.currentTarget.closest("li.section").dataset
- .instructionsId;
+ const section = event.currentTarget.closest("li.section");
+ const instructionsId = section.dataset.instructionsId;
// note: data will be validated/filtered by the server
const pipField = document.getElementById(`${this.formFieldId}_instructions${instructionsId}_pip`);
// ignore pressing button if no PIP has been selected
if (!pipField.value) {
return;
}
+ if (!this.validateVoidUsage(pipField, section, pipField.value, null)) {
+ return;
+ }
const valueField = document.getElementById(`${this.formFieldId}_instructions${instructionsId}_value`);
const runStandaloneField = document.getElementById(`${this.formFieldId}_instructions${instructionsId}_runStandalone`);
const applicationField = document.getElementById(`${this.formFieldId}_instructions${instructionsId}_application`);
@@ -262,8 +265,55 @@ define(["require", "exports", "tslib", "../../../../../../Core", "../../../../..
}
// toggle application selector
this.toggleApplicationFormField(instructionsId);
- const value = document.getElementById(`${this.formFieldId}_instructions${instructionsId}_value`);
- value.focus();
+ // hide value/runStandalone fields when the void instruction is selected
+ this.toggleValueAndRunStandaloneFormFields(instructionsId, pip !== "void");
+ if (pip !== "void") {
+ const value = document.getElementById(`${this.formFieldId}_instructions${instructionsId}_value`);
+ value.focus();
+ }
+ }
+ /**
+ * Toggles the visibility of the value and runStandalone form fields based on the selected pip.
+ */
+ toggleValueAndRunStandaloneFormFields(instructionsId, show) {
+ const valueDl = document.getElementById(`${this.formFieldId}_instructions${instructionsId}_value`).closest("dl");
+ const runStandaloneDl = document
+ .getElementById(`${this.formFieldId}_instructions${instructionsId}_runStandalone`)
+ .closest("dl");
+ if (show) {
+ Util_1.default.show(valueDl);
+ Util_1.default.show(runStandaloneDl);
+ }
+ else {
+ Util_1.default.hide(valueDl);
+ Util_1.default.hide(runStandaloneDl);
+ }
+ }
+ /**
+ * Validates that the `void` instruction is only used inside `update` sections and that it is
+ * the only instruction within its section. Returns `false` and shows an inline error if the
+ * placement is invalid.
+ */
+ validateVoidUsage(errorTarget, section, pip, excludedInstructionId) {
+ const instructionList = section.querySelector(".sortableList");
+ const existingInstructions = Array.from(instructionList.children);
+ const otherInstructions = existingInstructions.filter((li) => li.dataset.instructionId !== excludedInstructionId);
+ if (pip === "void") {
+ if (section.dataset.type !== "update") {
+ Util_1.default.innerError(errorTarget, Language.get("wcf.acp.devtools.project.instruction.error.voidInInstall"), true);
+ return false;
+ }
+ if (otherInstructions.length > 0) {
+ Util_1.default.innerError(errorTarget, Language.get("wcf.acp.devtools.project.instruction.error.voidNotAlone"), true);
+ return false;
+ }
+ }
+ else if (otherInstructions.some((li) => li.dataset.pip === "void")) {
+ Util_1.default.innerError(errorTarget, Language.get("wcf.acp.devtools.project.instruction.error.voidNotAlone"), true);
+ return false;
+ }
+ Util_1.default.innerError(errorTarget, "");
+ return true;
}
/**
* Opens a dialog to edit an existing instruction.
@@ -292,6 +342,10 @@ define(["require", "exports", "tslib", "../../../../../../Core", "../../../../..
pipSelect.value = pip;
const submit = () => {
const listItem = document.getElementById(`${this.formFieldId}_instruction${instructionId}`);
+ const section = listItem.closest("li.section");
+ if (!this.validateVoidUsage(pipSelect, section, pipSelect.value, instructionId)) {
+ return;
+ }
listItem.dataset.application =
Instructions.applicationPips.indexOf(pipSelect.value) !== -1 ? applicationSelect.value : "";
listItem.dataset.pip = pipSelect.value;
@@ -321,6 +375,14 @@ define(["require", "exports", "tslib", "../../../../../../Core", "../../../../..
else {
Util_1.default.hide(applicationSelect.closest("dl"));
}
+ if (pip === "void") {
+ Util_1.default.hide(valueInput.closest("dl"));
+ Util_1.default.hide(runStandaloneInput.closest("dl"));
+ }
+ else {
+ Util_1.default.show(valueInput.closest("dl"));
+ Util_1.default.show(runStandaloneInput.closest("dl"));
+ }
const description = DomTraverse.nextByTag(valueInput, "SMALL");
if (this.pipDefaultFilenames[pip] !== "") {
description.innerHTML = Language.get("wcf.acp.devtools.project.instruction.value.description.defaultFilename", {
diff --git a/wcfsetup/install/files/lib/acp/form/DevtoolsProjectAddForm.class.php b/wcfsetup/install/files/lib/acp/form/DevtoolsProjectAddForm.class.php
index 20d91a20694..ce6e01bafa9 100644
--- a/wcfsetup/install/files/lib/acp/form/DevtoolsProjectAddForm.class.php
+++ b/wcfsetup/install/files/lib/acp/form/DevtoolsProjectAddForm.class.php
@@ -725,6 +725,10 @@ protected function getInstructionValuesValidator()
}
foreach ($instructions['instructions'] as $instructionKey => $instruction) {
+ if ($instruction['pip'] === 'void') {
+ continue;
+ }
+
$value = $instruction['value'];
$packageInstallationPlugin = $packageInstallationPlugins[$instruction['pip']];
diff --git a/wcfsetup/install/files/lib/acp/form/DevtoolsProjectEditForm.class.php b/wcfsetup/install/files/lib/acp/form/DevtoolsProjectEditForm.class.php
index 45ca5eb3ca5..d04e2ecd56c 100644
--- a/wcfsetup/install/files/lib/acp/form/DevtoolsProjectEditForm.class.php
+++ b/wcfsetup/install/files/lib/acp/form/DevtoolsProjectEditForm.class.php
@@ -14,6 +14,7 @@
use wcf\system\form\builder\field\devtools\project\DevtoolsProjectRequiredPackagesFormField;
use wcf\system\form\builder\field\TextFormField;
use wcf\system\language\LanguageFactory;
+use wcf\system\package\PackageArchive;
use wcf\system\WCF;
/**
@@ -310,7 +311,7 @@ protected function setFormObjectData()
$versionUpdateInstructions[] = [
'application' => $instruction['attributes']['application'] ?? '',
'runStandalone' => isset($instruction['attributes']['run']) && $instruction['attributes']['run'] === 'standalone' ? 1 : 0,
- 'pip' => $instruction['pip'],
+ 'pip' => $instruction['pip'] === PackageArchive::VOID_MARKER ? 'void' : $instruction['pip'],
'value' => $instruction['value'],
];
}
diff --git a/wcfsetup/install/files/lib/system/devtools/package/DevtoolsPackageXmlWriter.class.php b/wcfsetup/install/files/lib/system/devtools/package/DevtoolsPackageXmlWriter.class.php
index 7a99d306c0d..29e9b1196a2 100644
--- a/wcfsetup/install/files/lib/system/devtools/package/DevtoolsPackageXmlWriter.class.php
+++ b/wcfsetup/install/files/lib/system/devtools/package/DevtoolsPackageXmlWriter.class.php
@@ -156,6 +156,11 @@ protected function writeInstructions()
$this->xmlWriter->startElement('instructions', $attributes);
foreach ($instructions['instructions'] as $instruction) {
+ if ($instruction['pip'] === 'void') {
+ $this->xmlWriter->writeElement('void', '', [], false);
+ continue;
+ }
+
$attributes = ['type' => $instruction['pip']];
if (isset($instruction['runStandalone']) && $instruction['runStandalone'] !== "0") {
$attributes['run'] = 'standalone';
diff --git a/wcfsetup/install/files/lib/system/form/builder/field/devtools/project/DevtoolsProjectInstructionsFormField.class.php b/wcfsetup/install/files/lib/system/form/builder/field/devtools/project/DevtoolsProjectInstructionsFormField.class.php
index 4ad9611ac9f..112fd48c55b 100644
--- a/wcfsetup/install/files/lib/system/form/builder/field/devtools/project/DevtoolsProjectInstructionsFormField.class.php
+++ b/wcfsetup/install/files/lib/system/form/builder/field/devtools/project/DevtoolsProjectInstructionsFormField.class.php
@@ -172,7 +172,11 @@ public function validate()
}
foreach ($instructions['instructions'] as $instruction) {
- if (!isset($instruction['pip']) || !isset($this->getPackageInstallationPlugins()[$instruction['pip']])) {
+ if (!isset($instruction['pip'])) {
+ return false;
+ }
+
+ if ($instruction['pip'] !== 'void' && !isset($this->getPackageInstallationPlugins()[$instruction['pip']])) {
return false;
}
@@ -190,6 +194,24 @@ public function validate()
$instruction['value'] = $instruction['value'] ?? '';
}
+ // the `void` instruction is only allowed in update sections and
+ // must be the only instruction
+ $hasVoid = false;
+ foreach ($instructions['instructions'] as $instruction) {
+ if ($instruction['pip'] === 'void') {
+ $hasVoid = true;
+ break;
+ }
+ }
+ if ($hasVoid) {
+ if ($instructions['type'] !== 'update') {
+ return false;
+ }
+ if (\count($instructions['instructions']) !== 1) {
+ return false;
+ }
+ }
+
return true;
}));
diff --git a/wcfsetup/install/lang/de.xml b/wcfsetup/install/lang/de.xml
index 9d3600f3b41..3bf8f0157b6 100644
--- a/wcfsetup/install/lang/de.xml
+++ b/wcfsetup/install/lang/de.xml
@@ -601,6 +601,8 @@
- {$directory} enthält keine XML-Dateien.]]>
+ - void-Anweisung ist nur für Aktualisierungsanweisungen zulässig.]]>
+ - void-Anweisung muss die einzige Anweisung in ihrem Anweisungssatz sein.]]>
- {$missingFile}{/implode}.]]>
- {$missingFile}{/implode}.]]>
diff --git a/wcfsetup/install/lang/en.xml b/wcfsetup/install/lang/en.xml
index 25050b2209c..5a3ae30c945 100644
--- a/wcfsetup/install/lang/en.xml
+++ b/wcfsetup/install/lang/en.xml
@@ -579,6 +579,8 @@
- {$directory} contains no XML files.]]>
+ - void instruction is only allowed for update instructions.]]>
+ - void instruction must be the only instruction in its set.]]>
- {$missingFile}{/implode}.]]>
- {$missingFile}{/implode}.]]>