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}.]]>