mirror of
https://github.com/nextcloud/server.git
synced 2026-02-03 20:41:22 -05:00
refactor(settings): use NcAppNavigation for the settings navigation
Migrate away from jQuery and Snap.js for the navigation. This is required to finally drop both dependencies. Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
This commit is contained in:
parent
87022e1ae1
commit
2e4ede0320
8 changed files with 152 additions and 132 deletions
|
|
@ -19,27 +19,6 @@ input {
|
|||
clear: both;
|
||||
}
|
||||
|
||||
/* icons for sidebar */
|
||||
.nav-icon-personal-settings {
|
||||
@include functions.icon-color('personal', 'settings', variables.$color-black);
|
||||
}
|
||||
|
||||
.nav-icon-security {
|
||||
@include functions.icon-color('toggle-filelist', 'settings', variables.$color-black);
|
||||
}
|
||||
|
||||
.nav-icon-clientsbox {
|
||||
@include functions.icon-color('change', 'settings', variables.$color-black);
|
||||
}
|
||||
|
||||
.nav-icon-federated-cloud {
|
||||
@include functions.icon-color('share', 'settings', variables.$color-black);
|
||||
}
|
||||
|
||||
.nav-icon-second-factor-backup-codes, .nav-icon-ssl-root-certificate {
|
||||
@include functions.icon-color('password', 'settings', variables.$color-black);
|
||||
}
|
||||
|
||||
#personal-settings-avatar-container {
|
||||
display: inline-grid;
|
||||
grid-template-columns: 1fr;
|
||||
|
|
@ -325,20 +304,6 @@ table.nostyle {
|
|||
}
|
||||
}
|
||||
|
||||
li.active {
|
||||
.delete,
|
||||
.rename {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.app-navigation-entry-utils {
|
||||
.delete,
|
||||
.rename {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
#usersearchform {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
|
|
@ -424,26 +389,6 @@ span.usersLastLoginTooltip {
|
|||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* SETTINGS NAVIGATION */
|
||||
#app-navigation {
|
||||
/* Navigation icons */
|
||||
img {
|
||||
margin-bottom: -3px;
|
||||
margin-inline-end: 6px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
li span.no-icon {
|
||||
padding-inline-start: 32px;
|
||||
}
|
||||
|
||||
ul li.active > span.utils {
|
||||
.delete, .rename {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* SETTINGS SECTIONS */
|
||||
// to match with NcSettingsSection component
|
||||
.section {
|
||||
|
|
|
|||
|
|
@ -49,22 +49,10 @@ trait CommonSettingsTrait {
|
|||
/** @var IInitialState */
|
||||
private $initialState;
|
||||
|
||||
/**
|
||||
* @return array{forms: array{personal: array, admin: array}}
|
||||
*/
|
||||
private function getNavigationParameters(string $currentType, string $currentSection): array {
|
||||
return [
|
||||
'forms' => [
|
||||
'personal' => $this->formatPersonalSections($currentType, $currentSection),
|
||||
'admin' => $this->formatAdminSections($currentType, $currentSection),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param IIconSection[][] $sections
|
||||
* @psalm-param 'admin'|'personal' $type
|
||||
* @return list<array{anchor: string, section-name: string, active: bool, icon: string}>
|
||||
* @return list<array{id: string, name: string, active: bool, icon: string}>
|
||||
*/
|
||||
protected function formatSections(array $sections, string $currentSection, string $type, string $currentType): array {
|
||||
$templateParameters = [];
|
||||
|
|
@ -89,8 +77,8 @@ trait CommonSettingsTrait {
|
|||
&& $type === $currentType;
|
||||
|
||||
$templateParameters[] = [
|
||||
'anchor' => $section->getID(),
|
||||
'section-name' => $section->getName(),
|
||||
'id' => $section->getID(),
|
||||
'name' => $section->getName(),
|
||||
'active' => $active,
|
||||
'icon' => $icon,
|
||||
];
|
||||
|
|
@ -99,11 +87,17 @@ trait CommonSettingsTrait {
|
|||
return $templateParameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{id: string, name: string, active: bool, icon: string}>
|
||||
*/
|
||||
protected function formatPersonalSections(string $currentType, string $currentSection): array {
|
||||
$sections = $this->settingsManager->getPersonalSections();
|
||||
return $this->formatSections($sections, $currentSection, 'personal', $currentType);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{id: string, name: string, active: bool, icon: string}>
|
||||
*/
|
||||
protected function formatAdminSections(string $currentType, string $currentSection): array {
|
||||
$sections = $this->settingsManager->getAdminSections();
|
||||
return $this->formatSections($sections, $currentSection, 'admin', $currentType);
|
||||
|
|
@ -175,9 +169,13 @@ trait CommonSettingsTrait {
|
|||
$this->initialState->provideInitialState('declarative-settings-forms', $declarativeSettings);
|
||||
}
|
||||
|
||||
$this->initialState->provideInitialState('sections', [
|
||||
'personal' => $this->formatPersonalSections($type, $section),
|
||||
'admin' => $this->formatAdminSections($type, $section),
|
||||
]);
|
||||
|
||||
$settings = array_merge(...$settings);
|
||||
$templateParams = $this->formatSettings($settings, $declarativeSettings);
|
||||
$templateParams = array_merge($templateParams, $this->getNavigationParameters($type, $section));
|
||||
|
||||
$activeSection = $this->settingsManager->getSection($type, $section);
|
||||
if ($activeSection) {
|
||||
|
|
@ -186,6 +184,7 @@ trait CommonSettingsTrait {
|
|||
$templateParams['activeSectionType'] = $type;
|
||||
}
|
||||
|
||||
Util::addScript(Application::APP_ID, 'main', prepend: true);
|
||||
return new TemplateResponse('settings', 'settings/frame', $templateParams);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
54
apps/settings/src/components/SettingsNavigationItem.vue
Normal file
54
apps/settings/src/components/SettingsNavigationItem.vue
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
<!--
|
||||
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<script setup lang="ts">
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { computed } from 'vue'
|
||||
import NcAppNavigationItem from '@nextcloud/vue/components/NcAppNavigationItem'
|
||||
|
||||
export interface ISettingsSection {
|
||||
id: string
|
||||
name: string
|
||||
icon?: string
|
||||
active: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
section: ISettingsSection
|
||||
type: 'admin' | 'personal'
|
||||
}>()
|
||||
|
||||
const href = computed(() => generateUrl('/settings/{type}/{section}', {
|
||||
type: props.type === 'personal' ? 'user' : 'admin',
|
||||
section: props.section.id,
|
||||
}))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NcAppNavigationItem
|
||||
:name="section.name"
|
||||
:active="section.active"
|
||||
:href="href">
|
||||
<template v-if="section.icon" #icon>
|
||||
<img
|
||||
:class="$style.settingsNavigationItem__icon"
|
||||
:src="section.icon"
|
||||
alt="">
|
||||
</template>
|
||||
</NcAppNavigationItem>
|
||||
</template>
|
||||
|
||||
<style module>
|
||||
.settingsNavigationItem__icon {
|
||||
width: var(--default-font-size);
|
||||
height: var(--default-font-size);
|
||||
object-fit: contain;
|
||||
filter: var(--background-invert-if-dark);
|
||||
}
|
||||
|
||||
:global(.active) .settingsNavigationItem__icon {
|
||||
filter: var(--primary-invert-if-dark);
|
||||
}
|
||||
</style>
|
||||
13
apps/settings/src/main-settings.ts
Normal file
13
apps/settings/src/main-settings.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
/*!
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { getCSPNonce } from '@nextcloud/auth'
|
||||
import Vue from 'vue'
|
||||
import SettingsNavigation from './views/SettingsNavigation.vue'
|
||||
|
||||
__webpack_nonce__ = getCSPNonce()
|
||||
|
||||
const app = new Vue(SettingsNavigation)
|
||||
app.$mount('#app-navigation')
|
||||
51
apps/settings/src/views/SettingsNavigation.vue
Normal file
51
apps/settings/src/views/SettingsNavigation.vue
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
<!--
|
||||
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ISettingsSection } from '../components/SettingsNavigationItem.vue'
|
||||
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import NcAppNavigation from '@nextcloud/vue/components/NcAppNavigation'
|
||||
import NcAppNavigationCaption from '@nextcloud/vue/components/NcAppNavigationCaption'
|
||||
import NcAppNavigationList from '@nextcloud/vue/components/NcAppNavigationList'
|
||||
import SettingsNavigationItem from '../components/SettingsNavigationItem.vue'
|
||||
|
||||
const {
|
||||
personal: personalSections,
|
||||
admin: adminSections,
|
||||
} = loadState<{ admin: ISettingsSection[], personal: ISettingsSection[] }>('settings', 'sections')
|
||||
const hasAdminSections = adminSections.length > 0
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NcAppNavigation>
|
||||
<NcAppNavigationCaption
|
||||
heading-id="settings-personal_section_heading"
|
||||
is-heading
|
||||
:name="t('settings', 'Personal')" />
|
||||
<NcAppNavigationList aria-labelledby="settings-personal_section_heading">
|
||||
<SettingsNavigationItem
|
||||
v-for="section in personalSections"
|
||||
:key="'personal-section--' + section.id"
|
||||
:section="section"
|
||||
type="personal" />
|
||||
</NcAppNavigationList>
|
||||
|
||||
<template v-if="hasAdminSections">
|
||||
<NcAppNavigationCaption
|
||||
heading-id="settings-admin_section_heading"
|
||||
is-heading
|
||||
:name="t('settings', 'Administration')" />
|
||||
<NcAppNavigationList aria-labelledby="settings-admin_section_heading">
|
||||
<SettingsNavigationItem
|
||||
v-for="section in adminSections"
|
||||
:key="'admin-section--' + section.id"
|
||||
:section="section"
|
||||
type="admin" />
|
||||
</NcAppNavigationList>
|
||||
</template>
|
||||
</NcAppNavigation>
|
||||
</template>
|
||||
|
|
@ -7,61 +7,7 @@
|
|||
style('settings', 'settings');
|
||||
?>
|
||||
|
||||
<div id="app-navigation">
|
||||
<?php if (!empty($_['forms']['admin'])): ?>
|
||||
<div id="app-navigation-caption-personal" class="app-navigation-caption"><?php p($l->t('Personal')); ?></div>
|
||||
<?php endif; ?>
|
||||
<nav class="app-navigation-personal" aria-labelledby="app-navigation-caption-personal">
|
||||
<ul>
|
||||
<?php foreach ($_['forms']['personal'] as $form) {
|
||||
if (isset($form['anchor'])) {
|
||||
$anchor = \OCP\Server::get(\OCP\IURLGenerator::class)->linkToRoute('settings.PersonalSettings.index', ['section' => $form['anchor']]);
|
||||
$class = 'nav-icon-' . $form['anchor'];
|
||||
$sectionName = $form['section-name']; ?>
|
||||
<li <?php print_unescaped($form['active'] ? ' class="active"' : ''); ?> data-section-id="<?php print_unescaped($form['anchor']); ?>" data-section-type="personal">
|
||||
<a href="<?php p($anchor); ?>"<?php print_unescaped($form['active'] ? ' aria-current="page"' : ''); ?>>
|
||||
<?php if (!empty($form['icon'])) { ?>
|
||||
<img alt="" src="<?php print_unescaped($form['icon']); ?>">
|
||||
<span><?php p($form['section-name']); ?></span>
|
||||
<?php } else { ?>
|
||||
<span class="no-icon"><?php p($form['section-name']); ?></span>
|
||||
<?php } ?>
|
||||
</a>
|
||||
</li>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
?>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<?php if (!empty($_['forms']['admin'])): ?>
|
||||
<div id="app-navigation-caption-administration" class="app-navigation-caption"><?php p($l->t('Administration')); ?></div>
|
||||
<?php endif; ?>
|
||||
<nav class="app-navigation-administration" aria-labelledby="app-navigation-caption-administration">
|
||||
<ul>
|
||||
<?php foreach ($_['forms']['admin'] as $form) {
|
||||
if (isset($form['anchor'])) {
|
||||
$anchor = \OCP\Server::get(\OCP\IURLGenerator::class)->linkToRoute('settings.AdminSettings.index', ['section' => $form['anchor']]);
|
||||
$class = 'nav-icon-' . $form['anchor'];
|
||||
$sectionName = $form['section-name']; ?>
|
||||
<li <?php print_unescaped($form['active'] ? ' class="active"' : ''); ?> data-section-id="<?php print_unescaped($form['anchor']); ?>" data-section-type="admin">
|
||||
<a href="<?php p($anchor); ?>"<?php print_unescaped($form['active'] ? ' aria-current="page"' : ''); ?>>
|
||||
<?php if (!empty($form['icon'])) { ?>
|
||||
<img alt="" src="<?php print_unescaped($form['icon']); ?>">
|
||||
<span><?php p($form['section-name']); ?></span>
|
||||
<?php } else { ?>
|
||||
<span class="no-icon"><?php p($form['section-name']); ?></span>
|
||||
<?php } ?>
|
||||
</a>
|
||||
</li>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
?>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
<div id="app-navigation"></div>
|
||||
<main id="app-content" <?php if (!empty($_['activeSectionId'])) { ?> data-active-section-id="<?php print_unescaped($_['activeSectionId']) ?>" <?php } if (!empty($_['activeSectionType'])) { ?> data-active-section-type="<?php print_unescaped($_['activeSectionType']) ?>" <?php } ?>>
|
||||
<?php print_unescaped($_['content']); ?>
|
||||
</main>
|
||||
|
|
|
|||
|
|
@ -122,12 +122,23 @@ class AdminSettingsControllerTest extends TestCase {
|
|||
->with($user, 'admin', 'test')
|
||||
->willReturn([]);
|
||||
|
||||
$idx = $this->adminSettingsController->index('test');
|
||||
$initialState = [];
|
||||
$this->initialState->expects(self::atLeastOnce())
|
||||
->method('provideInitialState')
|
||||
->willReturnCallback(function () use (&$initialState) {
|
||||
$initialState[] = func_get_args();
|
||||
});
|
||||
|
||||
$expected = new TemplateResponse('settings', 'settings/frame', [
|
||||
'forms' => ['personal' => [], 'admin' => []],
|
||||
'content' => ''
|
||||
]);
|
||||
$this->assertEquals($expected, $idx);
|
||||
$expected = new TemplateResponse(
|
||||
'settings',
|
||||
'settings/frame',
|
||||
[
|
||||
'content' => ''
|
||||
],
|
||||
);
|
||||
$this->assertEquals($expected, $this->adminSettingsController->index('test'));
|
||||
$this->assertEquals([
|
||||
['sections', ['admin' => [], 'personal' => []]],
|
||||
], $initialState);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ module.exports = {
|
|||
'public-nickname-handler': path.join(__dirname, 'apps/files_sharing/src', 'public-nickname-handler.ts'),
|
||||
},
|
||||
settings: {
|
||||
main: path.join(__dirname, 'apps/settings/src', 'main-settings.ts'),
|
||||
'vue-settings-admin-overview': path.join(__dirname, 'apps/settings/src', 'main-admin-overview.ts'),
|
||||
'vue-settings-admin-basic-settings': path.join(__dirname, 'apps/settings/src', 'main-admin-basic-settings.js'),
|
||||
'vue-settings-admin-ai': path.join(__dirname, 'apps/settings/src', 'main-admin-ai.js'),
|
||||
|
|
|
|||
Loading…
Reference in a new issue