feat(Settings): Add section to select preset
Signed-off-by: Louis Chemineau <louis@chmn.me>pull/54570/head
parent
9e9f3b9d16
commit
ed02d0df05
File diff suppressed because one or more lines are too long
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368\"><path d="m508-398 226-226-56-58-170 170-86-84-56 56 142 142ZM320-240q-33 0-56.5-23.5T240-320v-480q0-33 23.5-56.5T320-880h480q33 0 56.5 23.5T880-800v480q0 33-23.5 56.5T800-240H320Zm0-80h480v-480H320v480ZM160-80q-33 0-56.5-23.5T80-160v-560h80v560h560v80H160Zm160-720v480-480Z"/></svg>
|
||||
|
After Width: | Height: | Size: 390 B |
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
namespace OCA\Settings\Sections\Admin;
|
||||
|
||||
use OCP\IL10N;
|
||||
use OCP\IURLGenerator;
|
||||
use OCP\Settings\IIconSection;
|
||||
|
||||
class Presets implements IIconSection {
|
||||
|
||||
public function __construct(
|
||||
private IL10N $l,
|
||||
private IURLGenerator $urlGenerator,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getIcon(): string {
|
||||
return $this->urlGenerator->imagePath('settings', 'library_add_check.svg');
|
||||
}
|
||||
|
||||
public function getID(): string {
|
||||
return 'presets';
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
return $this->l->t('Settings presets');
|
||||
}
|
||||
|
||||
public function getPriority(): int {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
namespace OCA\Settings\Settings\Admin;
|
||||
|
||||
use OC\Config\PresetManager;
|
||||
use OCP\AppFramework\Http\TemplateResponse;
|
||||
use OCP\AppFramework\Services\IInitialState;
|
||||
use OCP\IConfig;
|
||||
use OCP\IL10N;
|
||||
use OCP\ServerVersion;
|
||||
use OCP\Settings\ISettings;
|
||||
|
||||
class Presets implements ISettings {
|
||||
public function __construct(
|
||||
private ServerVersion $serverVersion,
|
||||
private IConfig $config,
|
||||
private IL10N $l,
|
||||
private readonly PresetManager $presetManager,
|
||||
private IInitialState $initialState,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getForm() {
|
||||
$presets = $this->presetManager->retrieveLexiconPreset();
|
||||
$selectedPreset = $this->presetManager->getLexiconPreset();
|
||||
$presetsApps = $this->presetManager->retrieveLexiconPresetApps();
|
||||
|
||||
$this->initialState->provideInitialState('settings-selected-preset', $selectedPreset->name);
|
||||
$this->initialState->provideInitialState('settings-presets', $presets);
|
||||
$this->initialState->provideInitialState('settings-presets-apps', $presetsApps);
|
||||
|
||||
return new TemplateResponse('settings', 'settings/admin/presets', [], '');
|
||||
}
|
||||
|
||||
public function getSection() {
|
||||
return 'presets';
|
||||
}
|
||||
|
||||
public function getPriority() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public function getName(): ?string {
|
||||
return $this->l->t('Settings presets');
|
||||
}
|
||||
|
||||
public function getAuthorizedAppConfig(): array {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,160 @@
|
||||
<!--
|
||||
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
|
||||
|
||||
import type { PresetAppConfig, PresetAppConfigs, PresetAppsStates, PresetIds } from './models.ts'
|
||||
|
||||
const applicationsStates = loadState('settings', 'settings-presets-apps', {}) as PresetAppsStates
|
||||
|
||||
const props = defineProps({
|
||||
presets: {
|
||||
type: Object as () => PresetAppConfigs,
|
||||
required: true,
|
||||
},
|
||||
selectedPreset: {
|
||||
type: String as () => PresetIds,
|
||||
default: 'NONE',
|
||||
},
|
||||
})
|
||||
|
||||
const appsConfigPresets = Object.entries(props.presets)
|
||||
.map(([appId, presets]) => [appId, presets.filter(configPreset => configPreset.config === 'app')])
|
||||
.filter(([, presets]) => presets.length > 0) as [string, PresetAppConfig[]][]
|
||||
const userConfigPresets = Object.entries(props.presets)
|
||||
.map(([appId, presets]) => [appId, presets.filter(configPreset => configPreset.config === 'user')])
|
||||
.filter(([, presets]) => presets.length > 0) as [string, PresetAppConfig[]][]
|
||||
|
||||
const hasApplicationsPreset = computed(() => applicationsStates[props.selectedPreset].enabled.length > 0 || applicationsStates[props.selectedPreset].disabled.length > 0)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="presets">
|
||||
<h3 class="presets__title">
|
||||
{{ t('settings', 'Default config values') }}
|
||||
</h3>
|
||||
|
||||
<div v-if="appsConfigPresets.length > 0" class="presets__config-list">
|
||||
<h4 class="presets__config-list__subtitle">
|
||||
{{ t('settings', 'Applications config') }}
|
||||
</h4>
|
||||
<template v-for="[appId, appConfigPresets] in appsConfigPresets">
|
||||
<div v-for="configPreset in appConfigPresets"
|
||||
:key="appId + '-' + configPreset.entry.key"
|
||||
class="presets__config-list__item">
|
||||
<span>
|
||||
<div>{{ configPreset.entry.definition }}</div>
|
||||
<code class="presets__config-list__item__key">{{ configPreset.entry.key }}</code>
|
||||
</span>
|
||||
<span>
|
||||
<NcCheckboxRadioSwitch v-if="configPreset.entry.type === 'BOOL'"
|
||||
:model-value="configPreset.defaults[selectedPreset] === '1'"
|
||||
:disabled="true" />
|
||||
<code v-else>{{ configPreset.defaults[selectedPreset] }}</code>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div v-if="userConfigPresets.length > 0" class="presets__config-list">
|
||||
<h4 class="presets__config-list__subtitle">
|
||||
{{ t('settings', 'User config') }}
|
||||
</h4>
|
||||
<template v-for="[appId, userPresets] in userConfigPresets">
|
||||
<div v-for="configPreset in userPresets"
|
||||
:key="appId + '-' + configPreset.entry.key"
|
||||
class="presets__config-list__item">
|
||||
<span>
|
||||
<div>{{ configPreset.entry.definition }}</div>
|
||||
<code class="presets__config-list__item__key">{{ configPreset.entry.key }}</code>
|
||||
</span>
|
||||
<span>
|
||||
<NcCheckboxRadioSwitch v-if="configPreset.entry.type === 'BOOL'"
|
||||
:model-value="configPreset.defaults[selectedPreset] === '1'"
|
||||
:disabled="true" />
|
||||
<code v-else>{{ configPreset.defaults[selectedPreset] }}</code>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<template v-if="hasApplicationsPreset">
|
||||
<h3 class="presets__title">
|
||||
{{ t('settings', 'Bundled applications') }}
|
||||
</h3>
|
||||
|
||||
<div class="presets__app-list">
|
||||
<div class="presets__app-list__enabled">
|
||||
<h4 class="presets__app-list__title">
|
||||
{{ t('settings', 'Enabled applications') }}
|
||||
</h4>
|
||||
<ul>
|
||||
<li v-for="applicationId in applicationsStates[selectedPreset].enabled"
|
||||
:key="applicationId">
|
||||
{{ applicationId }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="presets__app-list__disabled">
|
||||
<h4 class="presets__app-list__title">
|
||||
{{ t('settings', 'Disabled applications') }}
|
||||
</h4>
|
||||
<ul>
|
||||
<li v-for="applicationId in applicationsStates[selectedPreset].disabled"
|
||||
:key="applicationId">
|
||||
{{ applicationId }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.presets {
|
||||
margin-top: 16px;
|
||||
|
||||
&__title {
|
||||
font-size: 16px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&__config-list {
|
||||
margin-top: 8px;
|
||||
width: 55%;
|
||||
|
||||
&__subtitle {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&__item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 2px 0;
|
||||
|
||||
&__key {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-maxcontrast);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__app-list {
|
||||
display: flex;
|
||||
gap: 32px;
|
||||
|
||||
&__title {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,118 @@
|
||||
<!--
|
||||
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<script setup lang="ts">
|
||||
import Domain from 'vue-material-design-icons/Domain.vue'
|
||||
import CloudCircleOutline from 'vue-material-design-icons/CloudCircleOutline.vue'
|
||||
import SchoolOutline from 'vue-material-design-icons/SchoolOutline.vue'
|
||||
import Crowd from 'vue-material-design-icons/Crowd.vue'
|
||||
import AccountGroupOutline from 'vue-material-design-icons/AccountGroupOutline.vue'
|
||||
import AccountOutline from 'vue-material-design-icons/AccountOutline.vue'
|
||||
import MinusCircleOutline from 'vue-material-design-icons/MinusCircleOutline.vue'
|
||||
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
|
||||
|
||||
import { type PresetAppConfigs, type PresetIds } from './models.ts'
|
||||
|
||||
const PresetNames = {
|
||||
LARGE: t('settings', 'Large organization'),
|
||||
MEDIUM: t('settings', 'Big organization'),
|
||||
SMALL: t('settings', 'Small organization'),
|
||||
SHARED: t('settings', 'Hosting company'),
|
||||
UNIVERSITY: t('settings', 'University'),
|
||||
SCHOOL: t('settings', 'School'),
|
||||
CLUB: t('settings', 'Club or association'),
|
||||
FAMILY: t('settings', 'Family'),
|
||||
PRIVATE: t('settings', 'Personal use'),
|
||||
NONE: t('settings', 'Default'),
|
||||
}
|
||||
|
||||
const PresetsIcons = {
|
||||
LARGE: Domain,
|
||||
MEDIUM: Domain,
|
||||
SMALL: Domain,
|
||||
SHARED: CloudCircleOutline,
|
||||
UNIVERSITY: SchoolOutline,
|
||||
SCHOOL: SchoolOutline,
|
||||
CLUB: AccountGroupOutline,
|
||||
FAMILY: Crowd,
|
||||
PRIVATE: AccountOutline,
|
||||
NONE: MinusCircleOutline,
|
||||
}
|
||||
|
||||
defineProps({
|
||||
presets: {
|
||||
type: Object as () => PresetAppConfigs,
|
||||
required: true,
|
||||
},
|
||||
value: {
|
||||
type: String as () => PresetIds,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'input', option: string): void
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form class="presets-form">
|
||||
<label v-for="(presetName, presetId) in PresetNames"
|
||||
:key="presetId"
|
||||
class="presets-form__option">
|
||||
|
||||
<components :is="PresetsIcons[presetId]" :size="32" />
|
||||
|
||||
<NcCheckboxRadioSwitch type="radio"
|
||||
:model-value="value"
|
||||
:value="presetId"
|
||||
name="preset"
|
||||
@update:modelValue="emit('input', presetId)" />
|
||||
|
||||
<span class="presets-form__option__name">{{ presetName }}</span>
|
||||
</label>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.presets-form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 24px;
|
||||
margin-top: 32px;
|
||||
|
||||
&__option {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
width: 250px;
|
||||
min-height: 100px;
|
||||
padding: 16px;
|
||||
border-radius: var(--border-radius-large);
|
||||
background-color: var(--color-background-dark);
|
||||
font-size: 20px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-background-darker);
|
||||
}
|
||||
|
||||
&:has(input[type=radio]:checked) {
|
||||
border: 2px solid var(--color-main-text);
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
&__name {
|
||||
flex-basis: 250px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
&__name, .material-design-icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,31 @@
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
type PresetAppConfigEntry = {
|
||||
key: string
|
||||
type: 'ARRAY' | 'BOOL' | 'FLOAT' | 'INT' | 'MIXED' | 'STRING'
|
||||
definition: string
|
||||
note: string
|
||||
lazy: boolean
|
||||
deprecated: boolean
|
||||
}
|
||||
|
||||
export type PresetIds = 'LARGE' | 'MEDIUM' | 'SMALL' | 'SHARED' | 'UNIVERSITY' | 'SCHOOL' | 'CLUB' | 'FAMILY' | 'PRIVATE' | 'NONE'
|
||||
|
||||
export type PresetAppConfig = {
|
||||
config: 'app' | 'user'
|
||||
entry: PresetAppConfigEntry
|
||||
defaults: Record<PresetIds, string>
|
||||
value?: unknown
|
||||
}
|
||||
|
||||
export type PresetAppConfigs = Record<string, PresetAppConfig[]>
|
||||
|
||||
type PresetAppsState = {
|
||||
enabled: string[]
|
||||
disabled: string[]
|
||||
}
|
||||
|
||||
export type PresetAppsStates = Record<PresetIds, PresetAppsState>
|
||||
@ -0,0 +1,19 @@
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import Vue from 'vue'
|
||||
|
||||
import SettingsPresets from './views/SettingsPresets.vue'
|
||||
import { getCSPNonce } from '@nextcloud/auth'
|
||||
|
||||
// CSP config for webpack dynamic chunk loading
|
||||
// eslint-disable-next-line camelcase
|
||||
__webpack_nonce__ = getCSPNonce()
|
||||
|
||||
export default new Vue({
|
||||
render: h => h(SettingsPresets),
|
||||
el: '#settings-presets',
|
||||
name: 'SettingsPresets',
|
||||
})
|
||||
@ -0,0 +1,68 @@
|
||||
<!--
|
||||
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import axios from '@nextcloud/axios'
|
||||
import NcSettingsSection from '@nextcloud/vue/components/NcSettingsSection'
|
||||
import NcButton from '@nextcloud/vue/components/NcButton'
|
||||
import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
|
||||
import PresetsSelectionForm from '../components/SettingsPresets/PresetsSelectionForm.vue'
|
||||
import PresetVisualisation from '../components/SettingsPresets/PresetVisualisation.vue'
|
||||
import type { PresetAppConfigs, PresetIds } from '../components/SettingsPresets/models'
|
||||
import logger from '../logger'
|
||||
|
||||
const presets = loadState('settings', 'settings-presets', {}) as PresetAppConfigs
|
||||
const currentPreset = ref(loadState('settings', 'settings-selected-preset', 'NONE') as PresetIds)
|
||||
const selectedPreset = ref(currentPreset.value)
|
||||
const savingPreset = ref(false)
|
||||
|
||||
async function saveSelectedPreset() {
|
||||
try {
|
||||
savingPreset.value = true
|
||||
await axios.post(generateUrl('/settings/preset/current'), {
|
||||
presetName: selectedPreset.value,
|
||||
})
|
||||
currentPreset.value = selectedPreset.value
|
||||
} catch (error) {
|
||||
showError(t('settings', 'Failed to save selected preset.'))
|
||||
logger.error('Error saving selected preset:', { error })
|
||||
selectedPreset.value = currentPreset.value
|
||||
} finally {
|
||||
savingPreset.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NcSettingsSection :name="t('settings', 'Settings presets')"
|
||||
:description="t('settings', 'Select a configuration preset for easy setup.')">
|
||||
<PresetsSelectionForm v-model="selectedPreset" :presets="presets" />
|
||||
|
||||
<PresetVisualisation :presets="presets" :selected-preset="selectedPreset" />
|
||||
|
||||
<NcButton class="save-button"
|
||||
variant="primary"
|
||||
:disabled="selectedPreset === currentPreset || savingPreset"
|
||||
@click="saveSelectedPreset()">
|
||||
{{ t('settings', 'Apply') }}
|
||||
|
||||
<template v-if="savingPreset" #icon>
|
||||
<NcLoadingIcon />
|
||||
</template>
|
||||
</NcButton>
|
||||
</NcSettingsSection>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.save-button {
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
\OCP\Util::addScript('settings', 'vue-settings-admin-settings-presets');
|
||||
|
||||
?>
|
||||
|
||||
<div id="settings-presets">
|
||||
</div>
|
||||
Loading…
Reference in New Issue