mirror of
https://github.com/nextcloud/server.git
synced 2026-02-03 20:41:22 -05:00
Merge fcb21bab4a into 24be39b0b9
This commit is contained in:
commit
feb4b50858
10 changed files with 377 additions and 9 deletions
|
|
@ -94,6 +94,7 @@ class AppSettingsController extends Controller {
|
|||
$this->initialState->provideInitialState('appstoreEnabled', $this->config->getSystemValueBool('appstoreenabled', true));
|
||||
$this->initialState->provideInitialState('appstoreBundles', $this->getBundles());
|
||||
$this->initialState->provideInitialState('appstoreUpdateCount', count($this->getAppsWithUpdates()));
|
||||
$this->initialState->provideInitialState('isAllInOne', filter_var(getenv('THIS_IS_AIO'), FILTER_VALIDATE_BOOL));
|
||||
|
||||
$groups = array_map(static fn (IGroup $group): array => [
|
||||
'id' => $group->getGID(),
|
||||
|
|
|
|||
|
|
@ -5,6 +5,11 @@
|
|||
|
||||
<template>
|
||||
<div id="app-content-inner">
|
||||
<OfficeSuiteSwitcher
|
||||
v-if="category === 'office'"
|
||||
:installed-apps="allApps"
|
||||
@suite-selected="onSuiteSelected" />
|
||||
|
||||
<div
|
||||
id="apps-list"
|
||||
class="apps-list"
|
||||
|
|
@ -150,6 +155,8 @@ import { subscribe, unsubscribe } from '@nextcloud/event-bus'
|
|||
import pLimit from 'p-limit'
|
||||
import NcButton from '@nextcloud/vue/components/NcButton'
|
||||
import AppItem from './AppList/AppItem.vue'
|
||||
import OfficeSuiteSwitcher from './AppList/OfficeSuiteSwitcher.vue'
|
||||
import { getOfficeSuiteById, OFFICE_SUITES } from '../constants/OfficeSuites.js'
|
||||
import logger from '../logger.ts'
|
||||
import AppManagement from '../mixins/AppManagement.js'
|
||||
import { useAppApiStore } from '../store/app-api-store.ts'
|
||||
|
|
@ -160,6 +167,7 @@ export default {
|
|||
components: {
|
||||
AppItem,
|
||||
NcButton,
|
||||
OfficeSuiteSwitcher,
|
||||
},
|
||||
|
||||
mixins: [AppManagement],
|
||||
|
|
@ -207,6 +215,11 @@ export default {
|
|||
return this.hasPendingUpdate && this.useListView
|
||||
},
|
||||
|
||||
allApps() {
|
||||
const exApps = this.$store.getters.isAppApiEnabled ? this.appApiStore.getAllApps : []
|
||||
return [...this.$store.getters.getAllApps, ...exApps]
|
||||
},
|
||||
|
||||
apps() {
|
||||
// Exclude ExApps from the list if AppAPI is disabled
|
||||
const exApps = this.$store.getters.isAppApiEnabled ? this.appApiStore.getAllApps : []
|
||||
|
|
@ -308,7 +321,7 @@ export default {
|
|||
},
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
beforeUnmount() {
|
||||
unsubscribe('nextcloud:unified-search.search', this.setSearch)
|
||||
unsubscribe('nextcloud:unified-search.reset', this.resetSearch)
|
||||
},
|
||||
|
|
@ -327,6 +340,40 @@ export default {
|
|||
this.search = ''
|
||||
},
|
||||
|
||||
async disableOfficeSuites(suites) {
|
||||
const disablePromises = suites.map((suite) => this.$store.dispatch('disableApp', { appId: suite.appId }).catch(() => {}))
|
||||
await Promise.all(disablePromises)
|
||||
},
|
||||
|
||||
async onSuiteSelected(suiteId) {
|
||||
logger.info('Office suite selected:', suiteId)
|
||||
|
||||
try {
|
||||
if (suiteId === null) {
|
||||
await this.disableOfficeSuites(OFFICE_SUITES)
|
||||
OC.Notification.showTemporary(t('settings', 'All office suites disabled'))
|
||||
return
|
||||
}
|
||||
|
||||
const selectedSuite = getOfficeSuiteById(suiteId)
|
||||
if (!selectedSuite) {
|
||||
logger.error('Unknown office suite selected:', suiteId)
|
||||
return
|
||||
}
|
||||
|
||||
await this.$store.dispatch('enableApp', { appId: selectedSuite.appId, groups: [] })
|
||||
OC.Notification.showTemporary(t('settings', '{name} enabled', { name: selectedSuite.name }))
|
||||
|
||||
const otherSuites = OFFICE_SUITES.filter((suite) => suite.id !== suiteId)
|
||||
await this.disableOfficeSuites(otherSuites)
|
||||
} catch (error) {
|
||||
logger.error('Error switching office suite:', error)
|
||||
if (error?.message) {
|
||||
OC.Notification.showTemporary(error.message)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
toggleBundle(id) {
|
||||
if (this.allBundlesEnabled(id)) {
|
||||
return this.disableBundle(id)
|
||||
|
|
|
|||
265
apps/settings/src/components/AppList/OfficeSuiteSwitcher.vue
Normal file
265
apps/settings/src/components/AppList/OfficeSuiteSwitcher.vue
Normal file
|
|
@ -0,0 +1,265 @@
|
|||
<!--
|
||||
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="office-suite-switcher">
|
||||
<div v-if="isAllInOne" class="office-suite-switcher__aio-message">
|
||||
<p>{{ t('settings', 'Office suite switching is managed through the Nextcloud All-in-One interface.') }}</p>
|
||||
<p>{{ t('settings', 'Please use the AIO interface to switch between office suites.') }}</p>
|
||||
</div>
|
||||
<template v-else>
|
||||
<p>{{ t('settings', 'Select your preferred office suite. Please note that installing requires manual server setup.') }}</p>
|
||||
<div class="office-suite-cards">
|
||||
<div
|
||||
v-for="suite in officeSuites"
|
||||
:key="suite.id"
|
||||
class="office-suite-card"
|
||||
:class="{
|
||||
'office-suite-card--primary': suite.isPrimary,
|
||||
'office-suite-card--selected': selectedSuite === suite.id,
|
||||
}"
|
||||
@click="selectSuite(suite.id)">
|
||||
<div class="office-suite-card__header">
|
||||
<h3 class="office-suite-card__title">
|
||||
{{ suite.name }}
|
||||
<span v-if="selectedSuite === suite.id">({{ t('settings', 'installed') }})</span>
|
||||
</h3>
|
||||
<IconCheckCircle v-if="selectedSuite === suite.id" class="office-suite-card__check" :size="24" />
|
||||
</div>
|
||||
<ul class="office-suite-card__features">
|
||||
<li v-for="(feature, index) in suite.features" :key="index">
|
||||
{{ t('settings', feature) }}
|
||||
</li>
|
||||
</ul>
|
||||
<a
|
||||
:href="suite.learnMoreUrl"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="office-suite-card__link"
|
||||
@click.stop>
|
||||
{{ t('settings', 'Learn more') }}
|
||||
<IconArrowRight :size="20" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="office-suite-actions">
|
||||
<button
|
||||
class="office-suite-disable-button"
|
||||
:disabled="!selectedSuite"
|
||||
@click="disableSuites">
|
||||
{{ t('settings', 'Disable office suites') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import IconArrowRight from 'vue-material-design-icons/ArrowRight.vue'
|
||||
import IconCheckCircle from 'vue-material-design-icons/CheckCircle.vue'
|
||||
import { OFFICE_SUITES } from '../../constants/OfficeSuites.js'
|
||||
|
||||
export default {
|
||||
name: 'OfficeSuiteSwitcher',
|
||||
|
||||
components: {
|
||||
IconCheckCircle,
|
||||
IconArrowRight,
|
||||
},
|
||||
|
||||
props: {
|
||||
installedApps: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
|
||||
emits: ['suite-selected'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
isAllInOne: loadState('settings', 'isAllInOne', false),
|
||||
selectedSuite: this.getInitialSuite(),
|
||||
officeSuites: OFFICE_SUITES,
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
t,
|
||||
getInitialSuite() {
|
||||
for (const suite of OFFICE_SUITES) {
|
||||
const app = this.installedApps.find((a) => a.id === suite.appId)
|
||||
if (app && app.active) {
|
||||
return suite.id
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
|
||||
selectSuite(suiteId) {
|
||||
if (this.selectedSuite === suiteId) {
|
||||
// already selected — keep selection; use the disable button to clear
|
||||
return
|
||||
}
|
||||
this.selectedSuite = suiteId
|
||||
this.$emit('suite-selected', suiteId)
|
||||
},
|
||||
|
||||
disableSuites() {
|
||||
if (this.selectedSuite === null) {
|
||||
return
|
||||
}
|
||||
this.selectedSuite = null
|
||||
this.$emit('suite-selected', null)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.office-suite-switcher {
|
||||
padding: 20px;
|
||||
margin-bottom: 30px;
|
||||
|
||||
&__aio-message {
|
||||
background-color: var(--color-background-dark);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-large);
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 8px 0;
|
||||
|
||||
&:first-child {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.office-suite-cards {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
max-width: 1200px;
|
||||
}
|
||||
|
||||
.office-suite-card {
|
||||
flex: 1;
|
||||
background-color: var(--color-main-background);
|
||||
border: 2px solid var(--color-border);
|
||||
border-radius: var(--border-radius-large);
|
||||
padding: 24px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
& * {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--color-primary-element);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
&--selected {
|
||||
background: linear-gradient(135deg, var(--color-primary-element-light) 0%, var(--color-main-background) 100%);
|
||||
color: var(--color-main-text);
|
||||
border-color: var(--color-primary-element);
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.office-suite-card--primary &__check {
|
||||
color: var(--color-primary-element);
|
||||
}
|
||||
|
||||
&__features {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0 0 20px 0;
|
||||
flex-grow: 1;
|
||||
|
||||
li {
|
||||
padding: 4px 0;
|
||||
padding-inline-start: 20px;
|
||||
position: relative;
|
||||
line-height: 1.5;
|
||||
|
||||
&::before {
|
||||
content: '•';
|
||||
position: absolute;
|
||||
inset-inline-start: 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--color-main-text);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
margin-top: auto;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.office-suite-card--selected &__link {
|
||||
color: var(--color-main-text);
|
||||
}
|
||||
}
|
||||
|
||||
.office-suite-actions {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.office-suite-disable-button {
|
||||
background: transparent;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-small);
|
||||
padding: 8px 12px;
|
||||
font-weight: 600;
|
||||
color: var(--color-main-text);
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease, border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.office-suite-disable-button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.office-suite-disable-button:hover:not(:disabled) {
|
||||
border-color: var(--color-primary-element);
|
||||
background: var(--color-background-dark);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.office-suite-cards {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
56
apps/settings/src/constants/OfficeSuites.js
Normal file
56
apps/settings/src/constants/OfficeSuites.js
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
export const OFFICE_SUITES = [
|
||||
{
|
||||
id: 'nextcloud-office',
|
||||
appId: 'richdocuments',
|
||||
name: 'Nextcloud Office',
|
||||
features: [
|
||||
t('settings', 'Best Nextcloud integration'),
|
||||
t('settings', 'Open source'),
|
||||
t('settings', 'Good performance'),
|
||||
t('settings', 'Best security: documents never leave your server'),
|
||||
t('settings', 'Best ODF compatibility'),
|
||||
t('settings', 'Best support for legacy files'),
|
||||
],
|
||||
learnMoreUrl: 'https://nextcloud.com/collaboraonline/',
|
||||
isPrimary: true,
|
||||
},
|
||||
{
|
||||
id: 'onlyoffice',
|
||||
appId: 'onlyoffice',
|
||||
name: 'Onlyoffice',
|
||||
features: [
|
||||
t('settings', 'Good Nextcloud integration'),
|
||||
t('settings', 'Open core'),
|
||||
t('settings', 'Best performance'),
|
||||
t('settings', 'Limited ODF compatibility'),
|
||||
t('settings', 'Best Microsoft compatibility'),
|
||||
],
|
||||
learnMoreUrl: 'https://nextcloud.com/onlyoffice/',
|
||||
isPrimary: false,
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* Get office suite configuration by ID
|
||||
*
|
||||
* @param {string} id - The suite ID
|
||||
* @return {object|undefined} The suite configuration or undefined if not found
|
||||
*/
|
||||
export function getOfficeSuiteById(id) {
|
||||
return OFFICE_SUITES.find((suite) => suite.id === id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get office suite configuration by app ID
|
||||
*
|
||||
* @param {string} appId - The app ID (richdocuments, onlyoffice, etc.)
|
||||
* @return {object|undefined} The suite configuration or undefined if not found
|
||||
*/
|
||||
export function getOfficeSuiteByAppId(appId) {
|
||||
return OFFICE_SUITES.find((suite) => suite.appId === appId)
|
||||
}
|
||||
|
|
@ -99,7 +99,6 @@
|
|||
</template>
|
||||
</NcAppNavigationItem>
|
||||
</template>
|
||||
|
||||
</template>
|
||||
</NcAppNavigation>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@
|
|||
|
||||
<!-- Tab content -->
|
||||
<AppDescriptionTab :app="app" />
|
||||
<AppDetailsTab :app="app" :key="app.id" />
|
||||
<AppDetailsTab :key="app.id" :app="app" />
|
||||
<AppReleasesTab :app="app" />
|
||||
<AppDeployDaemonTab :app="app" />
|
||||
</NcAppSidebar>
|
||||
|
|
|
|||
4
dist/settings-apps-view-4529.js
vendored
4
dist/settings-apps-view-4529.js
vendored
File diff suppressed because one or more lines are too long
2
dist/settings-apps-view-4529.js.map
vendored
2
dist/settings-apps-view-4529.js.map
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue