From be1022e1dac6fd04b433f6ca743c43d292cc5f83 Mon Sep 17 00:00:00 2001
From: Julius Knorr
Date: Wed, 14 Jan 2026 22:18:22 +0100
Subject: [PATCH 1/3] feat: Add office switcher with feature comparison
Signed-off-by: Julius Knorr
---
php/public/containers-form-submit.js | 48 ++++++
php/public/style.css | 152 ++++++++++++++++++
.../Controller/ConfigurationController.php | 24 +--
.../includes/optional-containers.twig | 109 ++++++++++---
php/templates/layout.twig | 2 +-
5 files changed, 299 insertions(+), 36 deletions(-)
diff --git a/php/public/containers-form-submit.js b/php/public/containers-form-submit.js
index b7ffd2d8..abd3fc68 100644
--- a/php/public/containers-form-submit.js
+++ b/php/public/containers-form-submit.js
@@ -12,6 +12,14 @@ document.addEventListener("DOMContentLoaded", function () {
const optionsContainersCheckboxes = document.querySelectorAll("#options-form input[type='checkbox']");
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 noneRadio = document.getElementById('office-none');
+ const collaboraHidden = document.getElementById('collabora');
+ const onlyofficeHidden = document.getElementById('onlyoffice');
+ let initialOfficeSelection = null;
+
optionsContainersCheckboxes.forEach(checkbox => {
initialStateOptionsContainers[checkbox.id] = checkbox.checked; // Use checked property to capture actual initial state
});
@@ -20,6 +28,17 @@ 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 && noneRadio) {
+ if (collaboraRadio.checked) {
+ initialOfficeSelection = 'collabora';
+ } else if (onlyofficeRadio.checked) {
+ initialOfficeSelection = 'onlyoffice';
+ } else {
+ initialOfficeSelection = 'none';
+ }
+ }
+
// Function to compare current states to initial states
function checkForOptionContainerChanges() {
let hasChanges = false;
@@ -30,6 +49,28 @@ document.addEventListener("DOMContentLoaded", function () {
}
});
+ // Check office suite changes and sync to hidden inputs
+ if (collaboraRadio && onlyofficeRadio && noneRadio && collaboraHidden && onlyofficeHidden) {
+ let currentOfficeSelection = null;
+ if (collaboraRadio.checked) {
+ currentOfficeSelection = 'collabora';
+ collaboraHidden.value = 'on';
+ onlyofficeHidden.value = '';
+ } else if (onlyofficeRadio.checked) {
+ currentOfficeSelection = 'onlyoffice';
+ collaboraHidden.value = '';
+ onlyofficeHidden.value = 'on';
+ } else {
+ currentOfficeSelection = 'none';
+ collaboraHidden.value = '';
+ onlyofficeHidden.value = '';
+ }
+
+ if (currentOfficeSelection !== initialOfficeSelection) {
+ hasChanges = true;
+ }
+ }
+
// Show or hide submit button based on changes
optionsFormSubmit.style.display = hasChanges ? 'block' : 'none';
}
@@ -82,6 +123,13 @@ document.addEventListener("DOMContentLoaded", function () {
// Initialize talk-recording visibility on page load
handleTalkVisibility(); // Ensure talk-recording is correctly initialized
+ // Add event listeners for office suite radio buttons
+ if (collaboraRadio && onlyofficeRadio && noneRadio) {
+ collaboraRadio.addEventListener('change', checkForOptionContainerChanges);
+ onlyofficeRadio.addEventListener('change', checkForOptionContainerChanges);
+ noneRadio.addEventListener('change', checkForOptionContainerChanges);
+ }
+
// Initial call to check for changes
checkForOptionContainerChanges();
checkForCommunityContainerChanges();
diff --git a/php/public/style.css b/php/public/style.css
index b4d5f8a5..7ac68be6 100644
--- a/php/public/style.css
+++ b/php/public/style.css
@@ -549,3 +549,155 @@ input[type="checkbox"]:disabled:not(:checked) + label {
#theme-toggle:not(:hover) #theme-icon {
opacity: 0.6; /* Slightly transparent */
}
+/* Office Suite Feature Cards */
+.office-suite-cards {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
+ gap: 16px;
+ margin: 20px 0;
+ align-items: stretch;
+}
+
+.office-radio {
+ display: none;
+}
+
+.office-card {
+ position: relative;
+ border: 2px solid var(--color-border-maxcontrast);
+ border-radius: var(--border-radius-large);
+ padding: 20px;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ background-color: var(--color-main-background);
+ display: flex;
+ flex-direction: column;
+}
+
+.office-card:hover {
+ border-color: var(--color-primary-element);
+ box-shadow: 0 4px 12px rgba(0, 130, 201, 0.15);
+ transform: translateY(-2px);
+}
+
+#office-collabora:checked + .office-card,
+#office-onlyoffice:checked + .office-card {
+ border-color: var(--color-nextcloud-blue);
+ background: linear-gradient(135deg, rgba(0, 130, 201, 0.08) 0%, rgba(0, 130, 201, 0.02) 100%);
+}
+
+[data-theme="dark"] #office-collabora:checked + .office-card,
+[data-theme="dark"] #office-onlyoffice:checked + .office-card {
+ background: linear-gradient(135deg, rgba(0, 145, 242, 0.15) 0%, rgba(0, 145, 242, 0.03) 100%);
+}
+
+.office-card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 16px;
+}
+
+.office-card h4 {
+ margin: 0;
+ height: 24px;
+ font-size: 18px;
+ font-weight: 600;
+ color: var(--color-main-text);
+}
+
+.office-checkmark {
+ flex-shrink: 0;
+ display: none;
+}
+
+#office-collabora:checked + .office-card .office-checkmark,
+#office-onlyoffice:checked + .office-card .office-checkmark {
+ display: block;
+}
+
+.office-features {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+.office-features li {
+ position: relative;
+ padding-left: 20px;
+ margin-bottom: 4px;
+ font-size: var(--default-font-size);
+ line-height: 1.5;
+ color: var(--color-main-text);
+}
+
+.office-features li::before {
+ content: '•';
+ position: absolute;
+ left: 6px;
+ color: var(--color-nextcloud-blue);
+ font-weight: bold;
+}
+
+.office-checkbox {
+ position: absolute;
+ opacity: 0;
+ pointer-events: none;
+}
+
+.office-learn-more {
+ display: inline-flex;
+ align-items: center;
+ margin-top: 12px;
+ color: var(--color-primary-element);
+ text-decoration: none;
+ font-size: var(--default-font-size);
+ font-weight: 500;
+ transition: color 0.2s ease;
+}
+
+.office-learn-more:hover {
+ color: var(--color-primary-element-hover);
+}
+
+.office-learn-more svg {
+ transition: transform 0.2s ease;
+}
+
+.office-learn-more:hover svg {
+ transform: translateX(3px);
+}
+
+.office-none-card {
+ text-align: center;
+ margin: 12px 0 20px 0;
+}
+
+.office-none-label {
+ display: inline-flex;
+ align-items: center;
+ font-size: 13px;
+ color: var(--color-primary-element);
+ cursor: pointer;
+ opacity: 0.7;
+ transition: opacity 0.2s ease;
+ padding: 8px 12px;
+ border-radius: var(--border-radius);
+}
+
+.office-none-label:hover {
+ opacity: 1;
+ background-color: var(--color-primary-element-light);
+}
+
+#office-none:checked + .office-none-label {
+ opacity: 1;
+ font-weight: 600;
+}
+
+/* Responsive adjustments for mobile */
+@media only screen and (max-width: 800px) {
+ .office-suite-cards {
+ grid-template-columns: 1fr;
+ }
+}
\ No newline at end of file
diff --git a/php/src/Controller/ConfigurationController.php b/php/src/Controller/ConfigurationController.php
index 45586f9c..b449db6a 100644
--- a/php/src/Controller/ConfigurationController.php
+++ b/php/src/Controller/ConfigurationController.php
@@ -76,24 +76,24 @@ readonly class ConfigurationController {
}
if (isset($request->getParsedBody()['options-form'])) {
- if (isset($request->getParsedBody()['collabora']) && isset($request->getParsedBody()['onlyoffice'])) {
- throw new InvalidSettingConfigurationException("Collabora and Onlyoffice are not allowed to be enabled at the same time!");
+ $officeSuiteChoice = $request->getParsedBody()['office_suite_choice'] ?? '';
+
+ if ($officeSuiteChoice === 'collabora') {
+ $this->configurationManager->SetCollaboraEnabledState(1);
+ $this->configurationManager->SetOnlyofficeEnabledState(0);
+ } elseif ($officeSuiteChoice === 'onlyoffice') {
+ $this->configurationManager->SetCollaboraEnabledState(0);
+ $this->configurationManager->SetOnlyofficeEnabledState(1);
+ } else {
+ $this->configurationManager->SetCollaboraEnabledState(0);
+ $this->configurationManager->SetOnlyofficeEnabledState(0);
}
+
if (isset($request->getParsedBody()['clamav'])) {
$this->configurationManager->SetClamavEnabledState(1);
} else {
$this->configurationManager->SetClamavEnabledState(0);
}
- if (isset($request->getParsedBody()['onlyoffice'])) {
- $this->configurationManager->SetOnlyofficeEnabledState(1);
- } else {
- $this->configurationManager->SetOnlyofficeEnabledState(0);
- }
- if (isset($request->getParsedBody()['collabora'])) {
- $this->configurationManager->SetCollaboraEnabledState(1);
- } else {
- $this->configurationManager->SetCollaboraEnabledState(0);
- }
if (isset($request->getParsedBody()['talk'])) {
$this->configurationManager->SetTalkEnabledState(1);
} else {
diff --git a/php/templates/includes/optional-containers.twig b/php/templates/includes/optional-containers.twig
index b4764592..f3739b04 100644
--- a/php/templates/includes/optional-containers.twig
+++ b/php/templates/includes/optional-containers.twig
@@ -23,20 +23,96 @@
>
-
+
Office Suite
+ Choose your preferred office suite. Only one can be enabled at a time.
+
-
-
+
+
+
+
+
+
+
+
+
+
+ Additional Optional Containers
-
-
-
-
+
AIO
-
+
From 90a0ae9b4672cbe412d44d7595e8bd9be77cec29 Mon Sep 17 00:00:00 2001
From: Julius Knorr
Date: Thu, 22 Jan 2026 12:14:07 +0100
Subject: [PATCH 2/3] fix: Increase max width of the settings container
Signed-off-by: Julius Knorr
---
php/public/style.css | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/php/public/style.css b/php/public/style.css
index 7ac68be6..238f58ba 100644
--- a/php/public/style.css
+++ b/php/public/style.css
@@ -28,7 +28,7 @@
--border-radius-large: 12px;
--default-font-size: 13px;
--checkbox-size: 16px;
- --max-width: 500px;
+ --max-width: 800px;
--container-top-margin: 20px;
--container-bottom-margin: 20px;
--container-padding: 2px;
@@ -37,9 +37,9 @@
--main-padding: 50px;
}
-/* Breakpoint calculation: 500px (max-width) + 100px (main-padding * 2) + 200px (additional space) = 800px
+/* Breakpoint calculation: 800px (max-width) + 100px (main-padding * 2) + 200px (additional space) = 1100px
Note: Unfortunately, it's not possible to calculate this dynamically using CSS variables in media queries */
-@media only screen and (max-width: 800px) {
+@media only screen and (max-width: 1100px) {
:root {
--container-top-margin: 50px;
--container-bottom-margin: 0px;
From 3391574c455ca72a0be040c4732498d32270682a Mon Sep 17 00:00:00 2001
From: Julius Knorr
Date: Thu, 22 Jan 2026 12:24:13 +0100
Subject: [PATCH 3/3] test: Adapt playwright test to new office selector
Signed-off-by: Julius Knorr
---
php/tests/tests/initial-setup.spec.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/php/tests/tests/initial-setup.spec.js b/php/tests/tests/initial-setup.spec.js
index c88cd8e3..eece723b 100644
--- a/php/tests/tests/initial-setup.spec.js
+++ b/php/tests/tests/initial-setup.spec.js
@@ -32,12 +32,12 @@ test('Initial setup', async ({ page: setupPage }) => {
await containersPage.locator('#talk').uncheck();
await containersPage.getByRole('checkbox', { name: 'Whiteboard' }).uncheck();
await containersPage.getByRole('checkbox', { name: 'Imaginary' }).uncheck();
- await containersPage.getByRole('checkbox', { name: 'Collabora' }).uncheck();
+ await containersPage.locator('#office-none').check();
await containersPage.getByRole('button', { name: 'Save changes' }).click();
await expect(containersPage.locator('#talk')).not.toBeChecked()
await expect(containersPage.getByRole('checkbox', { name: 'Whiteboard' })).not.toBeChecked()
await expect(containersPage.getByRole('checkbox', { name: 'Imaginary' })).not.toBeChecked()
- await expect(containersPage.getByRole('checkbox', { name: 'Collabora' })).not.toBeChecked()
+ await expect(containersPage.locator('#office-none')).toBeChecked()
// Reject invalid time zones
await containersPage.locator('#timezone').click();