Store office suite choice in a single config option

Previously it was three boolean options, which were supposed to be mutually
exclusive, which had to be taken care of manually.

Also previously there was only indirect indication of the choice "none", which
is brittle, too.

Signed-off-by: Pablo Zmdl <pablo@nextcloud.com>
This commit is contained in:
Pablo Zmdl
2026-07-09 13:15:02 +02:00
parent 4285168457
commit fd4e0ddcb2
8 changed files with 121 additions and 115 deletions
+7 -54
View File
@@ -1,6 +1,7 @@
document.addEventListener("DOMContentLoaded", function () {
const optionsForm = document.getElementById('options-form');
// Don't run if the expected form isn't present.
if (document.getElementById('options-form') === null) {
if (optionsForm === null) {
return;
}
@@ -20,14 +21,8 @@ document.addEventListener("DOMContentLoaded", function () {
const communityContainersCheckboxes = document.querySelectorAll("#community-form input[type='checkbox']");
// Office suite radio buttons
const collaboraRadio = document.getElementById('office-collabora');
const onlyofficeRadio = document.getElementById('office-onlyoffice');
const euroofficeRadio = document.getElementById('office-eurooffice');
const noneRadio = document.getElementById('office-none');
const collaboraHidden = document.getElementById('collabora');
const onlyofficeHidden = document.getElementById('onlyoffice');
const euroofficeHidden = document.getElementById('eurooffice');
let initialOfficeSelection = null;
const officeSuiteChoiceList = optionsForm.elements['office_suite_choice'];
const initialOfficeSelection = document.getElementById('initial-office-suite')?.value ?? '';
optionsContainersCheckboxes.forEach(checkbox => {
initialStateOptionsContainers[checkbox.id] = checkbox.checked; // Use checked property to capture actual initial state
@@ -37,19 +32,6 @@ document.addEventListener("DOMContentLoaded", function () {
initialStateCommunityContainers[checkbox.id] = checkbox.checked; // Use checked property to capture actual initial state
});
// Store initial office suite selection
if (collaboraRadio && onlyofficeRadio && euroofficeRadio && noneRadio) {
if (collaboraRadio.checked) {
initialOfficeSelection = 'collabora';
} else if (onlyofficeRadio.checked) {
initialOfficeSelection = 'onlyoffice';
} else if (euroofficeRadio.checked) {
initialOfficeSelection = 'eurooffice';
} else {
initialOfficeSelection = 'none';
}
}
// Function to compare current states to initial states
function checkForOptionContainerChanges() {
let hasChanges = false;
@@ -60,32 +42,8 @@ document.addEventListener("DOMContentLoaded", function () {
}
});
// Check office suite changes and sync to hidden inputs
if (collaboraRadio && onlyofficeRadio && euroofficeRadio && noneRadio && collaboraHidden && onlyofficeHidden && euroofficeHidden) {
let currentOfficeSelection = null;
if (collaboraRadio.checked) {
currentOfficeSelection = 'collabora';
collaboraHidden.value = 'on';
onlyofficeHidden.value = '';
euroofficeHidden.value = '';
} else if (onlyofficeRadio.checked) {
currentOfficeSelection = 'onlyoffice';
collaboraHidden.value = '';
onlyofficeHidden.value = 'on';
euroofficeHidden.value = '';
} else if (euroofficeRadio.checked) {
currentOfficeSelection = 'eurooffice';
collaboraHidden.value = '';
onlyofficeHidden.value = '';
euroofficeHidden.value = 'on';
} else {
currentOfficeSelection = 'none';
collaboraHidden.value = '';
onlyofficeHidden.value = '';
euroofficeHidden.value = '';
}
if (currentOfficeSelection !== initialOfficeSelection) {
if (officeSuiteChoiceList) {
if (officeSuiteChoiceList.value !== initialOfficeSelection) {
hasChanges = true;
}
}
@@ -156,12 +114,7 @@ document.addEventListener("DOMContentLoaded", function () {
handleTalkVisibility(); // Ensure talk-recording is correctly initialized
// Add event listeners for office suite radio buttons
if (collaboraRadio && onlyofficeRadio && euroofficeRadio && noneRadio) {
collaboraRadio.addEventListener('change', checkForOptionContainerChanges);
onlyofficeRadio.addEventListener('change', checkForOptionContainerChanges);
euroofficeRadio.addEventListener('change', checkForOptionContainerChanges);
noneRadio.addEventListener('change', checkForOptionContainerChanges);
}
officeSuiteChoiceList?.forEach((elem) => elem.addEventListener('change', checkForOptionContainerChanges));
// Initial call to check for changes
checkForOptionContainerChanges();
+1 -3
View File
@@ -153,9 +153,7 @@ $app->get('/containers', function (Request $request, Response $response, array $
'backup_times' => $configurationManager->getBackupTimes(),
'current_channel' => $dockerActionManager->GetCurrentChannel(),
'is_clamav_enabled' => $configurationManager->isClamavEnabled,
'is_onlyoffice_enabled' => $configurationManager->isOnlyofficeEnabled,
'is_eurooffice_enabled' => $configurationManager->isEuroofficeEnabled,
'is_collabora_enabled' => $configurationManager->isCollaboraEnabled,
'office_suite' => $configurationManager->getOfficeSuiteString(),
'is_talk_enabled' => $configurationManager->isTalkEnabled,
'borg_restore_password' => $configurationManager->borgRestorePassword,
'daily_backup_time' => $configurationManager->getDailyBackupTime(),
+7 -6
View File
@@ -12,6 +12,7 @@ use AIO\Container\ContainerVolume;
use AIO\Container\ContainerVolumes;
use AIO\Data\ConfigurationManager;
use AIO\Data\DataConst;
use AIO\Data\OfficeSuite;
use AIO\Docker\DockerActionManager;
readonly class ContainerDefinitionFetcher {
@@ -75,15 +76,15 @@ readonly class ContainerDefinitionFetcher {
continue;
}
} elseif ($entry['container_name'] === 'nextcloud-aio-onlyoffice') {
if (!$this->configurationManager->isOnlyofficeEnabled) {
if ($this->configurationManager->officeSuite !== OfficeSuite::Onlyoffice) {
continue;
}
} elseif ($entry['container_name'] === 'nextcloud-aio-eurooffice') {
if (!$this->configurationManager->isEuroofficeEnabled) {
if ($this->configurationManager->officeSuite !== OfficeSuite::Eurooffice) {
continue;
}
} elseif ($entry['container_name'] === 'nextcloud-aio-collabora') {
if (!$this->configurationManager->isCollaboraEnabled) {
if ($this->configurationManager->officeSuite !== OfficeSuite::Collabora) {
continue;
}
} elseif ($entry['container_name'] === 'nextcloud-aio-talk') {
@@ -192,15 +193,15 @@ readonly class ContainerDefinitionFetcher {
continue;
}
} elseif ($value === 'nextcloud-aio-onlyoffice') {
if (!$this->configurationManager->isOnlyofficeEnabled) {
if ($this->configurationManager->officeSuite !== OfficeSuite::Onlyoffice) {
continue;
}
} elseif ($value === 'nextcloud-aio-eurooffice') {
if (!$this->configurationManager->isEuroofficeEnabled) {
if ($this->configurationManager->officeSuite !== OfficeSuite::Eurooffice) {
continue;
}
} elseif ($value === 'nextcloud-aio-collabora') {
if (!$this->configurationManager->isCollaboraEnabled) {
if ($this->configurationManager->officeSuite !== OfficeSuite::Collabora) {
continue;
}
} elseif ($value === 'nextcloud-aio-talk') {
+5 -18
View File
@@ -5,6 +5,7 @@ namespace AIO\Controller;
use AIO\Data\ConfigurationManager;
use AIO\Data\InvalidSettingConfigurationException;
use AIO\Data\OfficeSuite;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
@@ -76,24 +77,10 @@ readonly class ConfigurationController {
}
if (isset($request->getParsedBody()['options-form'])) {
$officeSuiteChoice = $request->getParsedBody()['office_suite_choice'] ?? '';
if ($officeSuiteChoice === 'collabora') {
$this->configurationManager->isCollaboraEnabled = true;
$this->configurationManager->isOnlyofficeEnabled = false;
$this->configurationManager->isEuroofficeEnabled = false;
} elseif ($officeSuiteChoice === 'onlyoffice') {
$this->configurationManager->isCollaboraEnabled = false;
$this->configurationManager->isOnlyofficeEnabled = true;
$this->configurationManager->isEuroofficeEnabled = false;
} elseif ($officeSuiteChoice === 'eurooffice') {
$this->configurationManager->isCollaboraEnabled = false;
$this->configurationManager->isOnlyofficeEnabled = false;
$this->configurationManager->isEuroofficeEnabled = true;
} else {
$this->configurationManager->isCollaboraEnabled = false;
$this->configurationManager->isOnlyofficeEnabled = false;
$this->configurationManager->isEuroofficeEnabled = false;
if (isset($request->getParsedBody()['office_suite_choice'])) {
$inputValue = strval($request->getParsedBody()['office_suite_choice'] ?? '');
$officeSuite = OfficeSuite::tryFrom($inputValue) ?? OfficeSuite::None;
$this->configurationManager->officeSuite = $officeSuite;
}
$this->configurationManager->isClamavEnabled = isset($request->getParsedBody()['clamav']);
$this->configurationManager->isTalkEnabled = isset($request->getParsedBody()['talk']);
+2 -1
View File
@@ -11,6 +11,7 @@ use AIO\Docker\DockerActionManager;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use AIO\Data\ConfigurationManager;
use AIO\Data\OfficeSuite;
use Slim\Psr7\NonBufferedBody;
readonly class DockerController {
@@ -299,7 +300,7 @@ readonly class DockerController {
// This is a hack but no better solution was found for the meantime
// Stop Collabora first to make sure it force-saves
// See https://github.com/nextcloud/richdocuments/issues/3799
if ($id === self::TOP_CONTAINER && $this->configurationManager->isCollaboraEnabled) {
if ($id === self::TOP_CONTAINER && $this->configurationManager->officeSuite === OfficeSuite::Collabora) {
$this->PerformRecursiveContainerStop('nextcloud-aio-collabora', false, $addToStreamingResponseBody);
}
+75 -25
View File
@@ -95,22 +95,9 @@ class ConfigurationManager
set { $this->set('isClamavEnabled', $value); }
}
public bool $isOnlyofficeEnabled {
// Type-cast because old configs could have 1/0 for this key.
get => (bool) $this->get('isOnlyofficeEnabled', false);
set { $this->set('isOnlyofficeEnabled', $value); }
}
public bool $isEuroofficeEnabled {
// Only enabled if no other office suite is enabled.
get => !$this->isOtherOfficeThanEuroOfficeEnabled() && $this->get('isEuroofficeEnabled', true);
set { $this->set('isEuroofficeEnabled', $value); }
}
public bool $isCollaboraEnabled {
// Type-cast because old configs could have 1/0 for this key.
get => (bool) $this->get('isCollaboraEnabled', false);
set { $this->set('isCollaboraEnabled', $value); }
public OfficeSuite $officeSuite {
get => $this->readOfficeSuite();
set { $this->writeOfficeSuite($value); }
}
public bool $isTalkEnabled {
@@ -416,6 +403,75 @@ class ConfigurationManager
}
}
private function has(string $key) : bool {
return array_key_exists($key, $this->getConfig());
}
private function unset(string ...$keys) : void {
$changed = false;
$this->getConfig();
foreach ($keys as $key) {
if ($this->has($key)) {
unset($this->config[$key]);
$changed = true;
}
}
// Only write if this isn't called in between startTransaction() and commitTransaction().
if ($changed && $this->noWrite !== true) {
$this->writeConfig();
}
}
private function writeOfficeSuite(OfficeSuite $officeSuite) : void
{
$this->set('officeSuite', $officeSuite->value);
// Remove the deprecated options.
$this->unset('isCollaboraEnabled', 'isOnlyofficeEnabled', 'isEuroofficeEnabled');
}
private function readOfficeSuite() : OfficeSuite
{
if ($this->has('officeSuite')) {
$configValue = (string) $this->get('officeSuite', '');
return OfficeSuite::tryFrom($configValue) ?? OfficeSuite::None;
}
// Check the three boolean legacy options. Convert to boolean because very old configs could have
// `1`/`0` or even `"1"`/`"0"`/`""` as values.
if (boolval($this->get('isCollaboraEnabled', false)) === true) {
return OfficeSuite::Collabora;
}
if (boolval($this->get('isOnlyofficeEnabled', false)) === true) {
return OfficeSuite::Onlyoffice;
}
if (boolval($this->get('isEuroofficeEnabled', false)) === true) {
return OfficeSuite::Eurooffice;
}
// All offices disabled.
if (
$this->has('isCollaboraEnabled') && boolval($this->get('isCollaboraEnabled')) === false
&& $this->has('isOnlyofficeEnabled') && boolval($this->get('isOnlyofficeEnabled')) === false
&& (
// Eurooffice can be unset, which should be treated as `false`, too, because it means that
// the office choice wasn't ever saved after Eurooffice was introduced, but the user had
// previously disabled both available options, which means they want no office.
!$this->has('isEuroofficeEnabled')
|| boolval($this->get('isEuroofficeEnabled')) === false
)
) {
return OfficeSuite::None;
}
// Default
return OfficeSuite::Eurooffice;
}
public function getOfficeSuiteString() : string
{
return $this->officeSuite->value;
}
/**
* This allows to assign multiple attributes without saving the config to disk in between. It must be
* followed by a call to commitTransaction(), which then writes all changes to disk.
@@ -532,12 +588,6 @@ class ConfigurationManager
return '';
}
// Helper function for EuroOffice to make sure that it does not
// get enabled on existing instances after updating the default
private function isOtherOfficeThanEuroOfficeEnabled() : bool {
return $this->isCollaboraEnabled || $this->isOnlyofficeEnabled;
}
/**
* @throws InvalidSettingConfigurationException
*
@@ -1164,9 +1214,9 @@ class ConfigurationManager
'BACKUP_RESTORE_PASSWORD' => $this->borgRestorePassword,
'CLAMAV_ENABLED' => $this->isClamavEnabled ? 'yes' : '',
'TALK_RECORDING_ENABLED' => $this->isTalkRecordingEnabled ? 'yes' : '',
'ONLYOFFICE_ENABLED' => $this->isOnlyofficeEnabled ? 'yes' : '',
'EUROOFFICE_ENABLED' => $this->isEuroofficeEnabled ? 'yes' : '',
'COLLABORA_ENABLED' => $this->isCollaboraEnabled ? 'yes' : '',
'ONLYOFFICE_ENABLED' => $this->officeSuite === OfficeSuite::Onlyoffice ? 'yes' : '',
'EUROOFFICE_ENABLED' => $this->officeSuite === OfficeSuite::Eurooffice ? 'yes' : '',
'COLLABORA_ENABLED' => $this->officeSuite === OfficeSuite::Collabora ? 'yes' : '',
'TALK_ENABLED' => $this->isTalkEnabled ? 'yes' : '',
'UPDATE_NEXTCLOUD_APPS' => ($this->isDailyBackupRunning() && $this->areAutomaticUpdatesEnabled()) ? 'yes' : '',
'TIMEZONE' => $this->timezone === '' ? 'Etc/UTC' : $this->timezone,
+18
View File
@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace AIO\Data;
enum OfficeSuite: string
{
case Collabora = 'collabora';
case Onlyoffice = 'onlyoffice';
case Eurooffice = 'eurooffice';
case None = '';
}
@@ -20,7 +20,7 @@
name="office_suite_choice"
value="eurooffice"
class="office-radio"
{% if is_eurooffice_enabled == true %}
{% if office_suite == 'eurooffice' %}
checked="checked"
{% endif %}
>
@@ -49,14 +49,13 @@
</a>
{% endif %}
</label>
<input type="hidden" id="eurooffice" name="eurooffice" value="" data-initial-state="{% if is_eurooffice_enabled == true %}true{% else %}false{% endif %}">
<input
type="radio"
id="office-collabora"
name="office_suite_choice"
value="collabora"
class="office-radio"
{% if is_collabora_enabled == true %}
{% if office_suite == 'collabora' %}
checked="checked"
{% endif %}
>
@@ -86,14 +85,13 @@
</a>
{% endif %}
</label>
<input type="hidden" id="collabora" name="collabora" value="" data-initial-state="{% if is_collabora_enabled == true %}true{% else %}false{% endif %}">
<input
type="radio"
id="office-onlyoffice"
name="office_suite_choice"
value="onlyoffice"
class="office-radio"
{% if is_onlyoffice_enabled == true %}
{% if office_suite == 'onlyoffice' %}
checked="checked"
{% endif %}
>
@@ -123,7 +121,6 @@
</a>
{% endif %}
</label>
<input type="hidden" id="onlyoffice" name="onlyoffice" value="" data-initial-state="{% if is_onlyoffice_enabled == true %}true{% else %}false{% endif %}">
</div>
{% if isAnyRunning == false %}
<div class="office-none-card">
@@ -133,7 +130,7 @@
name="office_suite_choice"
value=""
class="office-radio"
{% if is_collabora_enabled == false and is_onlyoffice_enabled == false and is_eurooffice_enabled == false %}
{% if office_suite == '' %}
checked="checked"
{% endif %}
>
@@ -143,6 +140,7 @@
</label>
</div>
{% endif %}
<input type="hidden" id="initial-office-suite" value="{{ office_suite }}">
<input class="options-form-submit" type="submit" value="Save changes" />
<h3>Additional Optional Containers</h3>
<p>
@@ -270,7 +268,7 @@
<script type="text/javascript" src="disable-containers.js?v1"></script>
{% endif %}
{% if is_collabora_enabled == true and isAnyRunning == false and was_start_button_clicked == true %}
{% if office_suite == 'collabora' and isAnyRunning == false and was_start_button_clicked == true %}
<h3>Nextcloud Office dictionaries</h3>
{% if collabora_dictionaries == "" %}