feat(theming): Allow to configure default apps and app order in frontend settings
* Also add API for setting the value using ajax. * Add cypress tests for app order and defaul apps Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
This commit is contained in:
parent
363d9ebb13
commit
e9d4036389
|
@ -29,6 +29,11 @@
|
|||
*/
|
||||
return [
|
||||
'routes' => [
|
||||
[
|
||||
'name' => 'Theming#updateAppMenu',
|
||||
'url' => '/ajax/updateAppMenu',
|
||||
'verb' => 'PUT',
|
||||
],
|
||||
[
|
||||
'name' => 'Theming#updateStylesheet',
|
||||
'url' => '/ajax/updateStylesheet',
|
||||
|
|
|
@ -38,6 +38,7 @@
|
|||
*/
|
||||
namespace OCA\Theming\Controller;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use OCA\Theming\ImageManager;
|
||||
use OCA\Theming\Service\ThemesService;
|
||||
use OCA\Theming\ThemingDefaults;
|
||||
|
@ -180,6 +181,47 @@ class ThemingController extends Controller {
|
|||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @AuthorizedAdminSetting(settings=OCA\Theming\Settings\Admin)
|
||||
* @param string $setting
|
||||
* @param mixed $value
|
||||
* @return DataResponse
|
||||
* @throws NotPermittedException
|
||||
*/
|
||||
public function updateAppMenu($setting, $value) {
|
||||
$error = null;
|
||||
switch ($setting) {
|
||||
case 'defaultApps':
|
||||
if (is_array($value)) {
|
||||
try {
|
||||
$this->appManager->setDefaultApps($value);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
$error = $this->l10n->t('Invalid app given');
|
||||
}
|
||||
} else {
|
||||
$error = $this->l10n->t('Invalid type for setting "defaultApp" given');
|
||||
}
|
||||
break;
|
||||
default:
|
||||
$error = $this->l10n->t('Invalid setting key');
|
||||
}
|
||||
if ($error !== null) {
|
||||
return new DataResponse([
|
||||
'data' => [
|
||||
'message' => $error,
|
||||
],
|
||||
'status' => 'error'
|
||||
], Http::STATUS_BAD_REQUEST);
|
||||
}
|
||||
|
||||
return new DataResponse([
|
||||
'data' => [
|
||||
'message' => $this->l10n->t('Saved'),
|
||||
],
|
||||
'status' => 'success'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that a string is a valid http/https url
|
||||
*/
|
||||
|
@ -299,6 +341,7 @@ class ThemingController extends Controller {
|
|||
*/
|
||||
public function undoAll(): DataResponse {
|
||||
$this->themingDefaults->undoAll();
|
||||
$this->appManager->setDefaultApps([]);
|
||||
|
||||
return new DataResponse(
|
||||
[
|
||||
|
|
|
@ -26,23 +26,34 @@ declare(strict_types=1);
|
|||
namespace OCA\Theming\Listener;
|
||||
|
||||
use OCA\Theming\AppInfo\Application;
|
||||
use OCP\App\IAppManager;
|
||||
use OCP\Config\BeforePreferenceDeletedEvent;
|
||||
use OCP\Config\BeforePreferenceSetEvent;
|
||||
use OCP\EventDispatcher\Event;
|
||||
use OCP\EventDispatcher\IEventListener;
|
||||
|
||||
class BeforePreferenceListener implements IEventListener {
|
||||
public function __construct(
|
||||
private IAppManager $appManager,
|
||||
) {
|
||||
}
|
||||
|
||||
public function handle(Event $event): void {
|
||||
if (!$event instanceof BeforePreferenceSetEvent
|
||||
&& !$event instanceof BeforePreferenceDeletedEvent) {
|
||||
// Invalid event type
|
||||
return;
|
||||
}
|
||||
|
||||
if ($event->getAppId() !== Application::APP_ID) {
|
||||
return;
|
||||
switch ($event->getAppId()) {
|
||||
case Application::APP_ID: $this->handleThemingValues($event); break;
|
||||
case 'core': $this->handleCoreValues($event); break;
|
||||
}
|
||||
}
|
||||
|
||||
private function handleThemingValues(BeforePreferenceSetEvent|BeforePreferenceDeletedEvent $event): void {
|
||||
if ($event->getConfigKey() !== 'shortcuts_disabled') {
|
||||
// Not allowed config key
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -53,4 +64,27 @@ class BeforePreferenceListener implements IEventListener {
|
|||
|
||||
$event->setValid(true);
|
||||
}
|
||||
|
||||
private function handleCoreValues(BeforePreferenceSetEvent|BeforePreferenceDeletedEvent $event): void {
|
||||
if ($event->getConfigKey() !== 'apporder') {
|
||||
// Not allowed config key
|
||||
return;
|
||||
}
|
||||
|
||||
if ($event instanceof BeforePreferenceDeletedEvent) {
|
||||
$event->setValid(true);
|
||||
return;
|
||||
}
|
||||
|
||||
$value = json_decode($event->getConfigValue(), true, flags:JSON_THROW_ON_ERROR);
|
||||
if (is_array(($value))) {
|
||||
foreach ($value as $appName => $order) {
|
||||
if (!$this->appManager->isEnabledForUser($appName) || !is_array($order) || empty($order) || !is_numeric($order[key($order)])) {
|
||||
// Invalid config value, refuse the change
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
$event->setValid(true);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,28 +40,16 @@ use OCP\Settings\IDelegatedSettings;
|
|||
use OCP\Util;
|
||||
|
||||
class Admin implements IDelegatedSettings {
|
||||
private string $appName;
|
||||
private IConfig $config;
|
||||
private IL10N $l;
|
||||
private ThemingDefaults $themingDefaults;
|
||||
private IInitialState $initialState;
|
||||
private IURLGenerator $urlGenerator;
|
||||
private ImageManager $imageManager;
|
||||
|
||||
public function __construct(string $appName,
|
||||
IConfig $config,
|
||||
IL10N $l,
|
||||
ThemingDefaults $themingDefaults,
|
||||
IInitialState $initialState,
|
||||
IURLGenerator $urlGenerator,
|
||||
ImageManager $imageManager) {
|
||||
$this->appName = $appName;
|
||||
$this->config = $config;
|
||||
$this->l = $l;
|
||||
$this->themingDefaults = $themingDefaults;
|
||||
$this->initialState = $initialState;
|
||||
$this->urlGenerator = $urlGenerator;
|
||||
$this->imageManager = $imageManager;
|
||||
public function __construct(
|
||||
private string $appName,
|
||||
private IConfig $config,
|
||||
private IL10N $l,
|
||||
private ThemingDefaults $themingDefaults,
|
||||
private IInitialState $initialState,
|
||||
private IURLGenerator $urlGenerator,
|
||||
private ImageManager $imageManager,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -80,7 +68,7 @@ class Admin implements IDelegatedSettings {
|
|||
$carry[$key] = $this->imageManager->getSupportedUploadImageFormats($key);
|
||||
return $carry;
|
||||
}, []);
|
||||
|
||||
|
||||
$this->initialState->provideInitialState('adminThemingParameters', [
|
||||
'isThemable' => $themable,
|
||||
'notThemableErrorMessage' => $errorMessage,
|
||||
|
@ -89,6 +77,7 @@ class Admin implements IDelegatedSettings {
|
|||
'slogan' => $this->themingDefaults->getSlogan(),
|
||||
'color' => $this->themingDefaults->getDefaultColorPrimary(),
|
||||
'logoMime' => $this->config->getAppValue(Application::APP_ID, 'logoMime', ''),
|
||||
'allowedMimeTypes' => $allowedMimeTypes,
|
||||
'backgroundMime' => $this->config->getAppValue(Application::APP_ID, 'backgroundMime', ''),
|
||||
'logoheaderMime' => $this->config->getAppValue(Application::APP_ID, 'logoheaderMime', ''),
|
||||
'faviconMime' => $this->config->getAppValue(Application::APP_ID, 'faviconMime', ''),
|
||||
|
@ -98,7 +87,7 @@ class Admin implements IDelegatedSettings {
|
|||
'docUrlIcons' => $this->urlGenerator->linkToDocs('admin-theming-icons'),
|
||||
'canThemeIcons' => $this->imageManager->shouldReplaceIcons(),
|
||||
'userThemingDisabled' => $this->themingDefaults->isUserThemingDisabled(),
|
||||
'allowedMimeTypes' => $allowedMimeTypes,
|
||||
'defaultApps' => array_filter(explode(',', $this->config->getSystemValueString('defaultapp', ''))),
|
||||
]);
|
||||
|
||||
Util::addScript($this->appName, 'admin-theming');
|
||||
|
|
|
@ -28,6 +28,7 @@ namespace OCA\Theming\Settings;
|
|||
use OCA\Theming\ITheme;
|
||||
use OCA\Theming\Service\ThemesService;
|
||||
use OCA\Theming\ThemingDefaults;
|
||||
use OCP\App\IAppManager;
|
||||
use OCP\AppFramework\Http\TemplateResponse;
|
||||
use OCP\AppFramework\Services\IInitialState;
|
||||
use OCP\IConfig;
|
||||
|
@ -36,22 +37,15 @@ use OCP\Util;
|
|||
|
||||
class Personal implements ISettings {
|
||||
|
||||
protected string $appName;
|
||||
private IConfig $config;
|
||||
private ThemesService $themesService;
|
||||
private IInitialState $initialStateService;
|
||||
private ThemingDefaults $themingDefaults;
|
||||
|
||||
public function __construct(string $appName,
|
||||
IConfig $config,
|
||||
ThemesService $themesService,
|
||||
IInitialState $initialStateService,
|
||||
ThemingDefaults $themingDefaults) {
|
||||
$this->appName = $appName;
|
||||
$this->config = $config;
|
||||
$this->themesService = $themesService;
|
||||
$this->initialStateService = $initialStateService;
|
||||
$this->themingDefaults = $themingDefaults;
|
||||
public function __construct(
|
||||
protected string $appName,
|
||||
private string $userId,
|
||||
private IConfig $config,
|
||||
private ThemesService $themesService,
|
||||
private IInitialState $initialStateService,
|
||||
private ThemingDefaults $themingDefaults,
|
||||
private IAppManager $appManager,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getForm(): TemplateResponse {
|
||||
|
@ -74,9 +68,13 @@ class Personal implements ISettings {
|
|||
});
|
||||
}
|
||||
|
||||
// Get the default app enforced by admin
|
||||
$forcedDefaultApp = $this->appManager->getDefaultAppForUser(null, false);
|
||||
|
||||
$this->initialStateService->provideInitialState('themes', array_values($themes));
|
||||
$this->initialStateService->provideInitialState('enforceTheme', $enforcedTheme);
|
||||
$this->initialStateService->provideInitialState('isUserThemingDisabled', $this->themingDefaults->isUserThemingDisabled());
|
||||
$this->initialStateService->provideInitialState('enforcedDefaultApp', $forcedDefaultApp);
|
||||
|
||||
Util::addScript($this->appName, 'personal-theming');
|
||||
|
||||
|
|
|
@ -106,6 +106,7 @@
|
|||
</a>
|
||||
</div>
|
||||
</NcSettingsSection>
|
||||
<AppMenuSection :default-apps.sync="defaultApps" />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
|
@ -118,6 +119,7 @@ import CheckboxField from './components/admin/CheckboxField.vue'
|
|||
import ColorPickerField from './components/admin/ColorPickerField.vue'
|
||||
import FileInputField from './components/admin/FileInputField.vue'
|
||||
import TextField from './components/admin/TextField.vue'
|
||||
import AppMenuSection from './components/admin/AppMenuSection.vue'
|
||||
|
||||
const {
|
||||
backgroundMime,
|
||||
|
@ -136,6 +138,7 @@ const {
|
|||
slogan,
|
||||
url,
|
||||
userThemingDisabled,
|
||||
defaultApps,
|
||||
} = loadState('theming', 'adminThemingParameters')
|
||||
|
||||
const textFields = [
|
||||
|
@ -247,6 +250,7 @@ export default {
|
|||
name: 'AdminTheming',
|
||||
|
||||
components: {
|
||||
AppMenuSection,
|
||||
CheckboxField,
|
||||
ColorPickerField,
|
||||
FileInputField,
|
||||
|
@ -259,6 +263,8 @@ export default {
|
|||
'update:theming',
|
||||
],
|
||||
|
||||
textFields,
|
||||
|
||||
data() {
|
||||
return {
|
||||
textFields,
|
||||
|
@ -267,6 +273,7 @@ export default {
|
|||
advancedTextFields,
|
||||
advancedFileInputFields,
|
||||
userThemingField,
|
||||
defaultApps,
|
||||
|
||||
canThemeIcons,
|
||||
docUrl,
|
||||
|
|
|
@ -75,6 +75,8 @@
|
|||
{{ t('theming', 'Disable all keyboard shortcuts') }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
</NcSettingsSection>
|
||||
|
||||
<UserAppMenuSection />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
|
@ -87,6 +89,7 @@ import NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection.
|
|||
|
||||
import BackgroundSettings from './components/BackgroundSettings.vue'
|
||||
import ItemPreview from './components/ItemPreview.vue'
|
||||
import UserAppMenuSection from './components/UserAppMenuSection.vue'
|
||||
|
||||
const availableThemes = loadState('theming', 'themes', [])
|
||||
const enforceTheme = loadState('theming', 'enforceTheme', '')
|
||||
|
@ -94,8 +97,6 @@ const shortcutsDisabled = loadState('theming', 'shortcutsDisabled', false)
|
|||
|
||||
const isUserThemingDisabled = loadState('theming', 'isUserThemingDisabled')
|
||||
|
||||
console.debug('Available themes', availableThemes)
|
||||
|
||||
export default {
|
||||
name: 'UserThemes',
|
||||
|
||||
|
@ -104,6 +105,7 @@ export default {
|
|||
NcCheckboxRadioSwitch,
|
||||
NcSettingsSection,
|
||||
BackgroundSettings,
|
||||
UserAppMenuSection,
|
||||
},
|
||||
|
||||
data() {
|
||||
|
|
|
@ -0,0 +1,130 @@
|
|||
<template>
|
||||
<ol ref="listElement" data-cy-app-order class="order-selector">
|
||||
<AppOrderSelectorElement v-for="app,index in appList"
|
||||
:key="`${app.id}${renderCount}`"
|
||||
:app="app"
|
||||
:is-first="index === 0 || !!appList[index - 1].default"
|
||||
:is-last="index === value.length - 1"
|
||||
v-on="app.default ? {} : {
|
||||
'move:up': () => moveUp(index),
|
||||
'move:down': () => moveDown(index),
|
||||
}" />
|
||||
</ol>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { useSortable } from '@vueuse/integrations/useSortable'
|
||||
import { PropType, computed, defineComponent, ref } from 'vue'
|
||||
|
||||
import AppOrderSelectorElement from './AppOrderSelectorElement.vue'
|
||||
|
||||
interface IApp {
|
||||
id: string // app id
|
||||
icon: string // path to the icon svg
|
||||
label?: string // display name
|
||||
default?: boolean // force app as default app
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'AppOrderSelector',
|
||||
components: {
|
||||
AppOrderSelectorElement,
|
||||
},
|
||||
props: {
|
||||
/**
|
||||
* List of apps to reorder
|
||||
*/
|
||||
value: {
|
||||
type: Array as PropType<IApp[]>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: {
|
||||
/**
|
||||
* Update the apps list on reorder
|
||||
* @param value The new value of the app list
|
||||
*/
|
||||
'update:value': (value: IApp[]) => Array.isArray(value),
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
/**
|
||||
* The Element that contains the app list
|
||||
*/
|
||||
const listElement = ref<HTMLElement | null>(null)
|
||||
|
||||
/**
|
||||
* The app list with setter that will ement the `update:value` event
|
||||
*/
|
||||
const appList = computed({
|
||||
get: () => props.value,
|
||||
// Ensure the sortable.js does not mess with the default attribute
|
||||
set: (list) => {
|
||||
const newValue = [...list].sort((a, b) => ((b.default ? 1 : 0) - (a.default ? 1 : 0)) || list.indexOf(a) - list.indexOf(b))
|
||||
if (newValue.some(({ id }, index) => id !== props.value[index].id)) {
|
||||
emit('update:value', newValue)
|
||||
} else {
|
||||
// forceUpdate as the DOM has changed because of a drag event, but the reactive state has not -> wrong state
|
||||
renderCount.value += 1
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* Helper to force rerender the list in case of a invalid drag event
|
||||
*/
|
||||
const renderCount = ref(0)
|
||||
|
||||
/**
|
||||
* Handle drag & drop sorting
|
||||
*/
|
||||
useSortable(listElement, appList, { filter: '.order-selector-element--disabled' })
|
||||
|
||||
/**
|
||||
* Handle element is moved up
|
||||
* @param index The index of the element that is moved
|
||||
*/
|
||||
const moveUp = (index: number) => {
|
||||
const before = index > 1 ? props.value.slice(0, index - 1) : []
|
||||
// skip if not possible, because of default default app
|
||||
if (props.value[index - 1]?.default) {
|
||||
return
|
||||
}
|
||||
|
||||
const after = [props.value[index - 1]]
|
||||
if (index < props.value.length - 1) {
|
||||
after.push(...props.value.slice(index + 1))
|
||||
}
|
||||
emit('update:value', [...before, props.value[index], ...after])
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle element is moved down
|
||||
* @param index The index of the element that is moved
|
||||
*/
|
||||
const moveDown = (index: number) => {
|
||||
const before = index > 0 ? props.value.slice(0, index) : []
|
||||
before.push(props.value[index + 1])
|
||||
|
||||
const after = index < (props.value.length - 2) ? props.value.slice(index + 2) : []
|
||||
emit('update:value', [...before, props.value[index], ...after])
|
||||
}
|
||||
|
||||
return {
|
||||
appList,
|
||||
listElement,
|
||||
|
||||
moveDown,
|
||||
moveUp,
|
||||
|
||||
renderCount,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.order-selector {
|
||||
width: max-content;
|
||||
min-width: 260px; // align with NcSelect
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,145 @@
|
|||
<template>
|
||||
<li :data-cy-app-order-element="app.id"
|
||||
:class="{
|
||||
'order-selector-element': true,
|
||||
'order-selector-element--disabled': app.default
|
||||
}">
|
||||
<svg width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
role="presentation">
|
||||
<image preserveAspectRatio="xMinYMin meet"
|
||||
x="0"
|
||||
y="0"
|
||||
width="20"
|
||||
height="20"
|
||||
:xlink:href="app.icon"
|
||||
class="order-selector-element__icon" />
|
||||
</svg>
|
||||
|
||||
<div class="order-selector-element__label">
|
||||
{{ app.label ?? app.id }}
|
||||
</div>
|
||||
|
||||
<div class="order-selector-element__actions">
|
||||
<NcButton v-show="!isFirst && !app.default"
|
||||
:aria-label="t('settings', 'Move up')"
|
||||
data-cy-app-order-button="up"
|
||||
type="tertiary-no-background"
|
||||
@click="$emit('move:up')">
|
||||
<template #icon>
|
||||
<IconArrowUp :size="20" />
|
||||
</template>
|
||||
</NcButton>
|
||||
<div v-show="isFirst || !!app.default" aria-hidden="true" class="order-selector-element__placeholder" />
|
||||
<NcButton v-show="!isLast && !app.default"
|
||||
:aria-label="t('settings', 'Move down')"
|
||||
data-cy-app-order-button="down"
|
||||
type="tertiary-no-background"
|
||||
@click="$emit('move:down')">
|
||||
<template #icon>
|
||||
<IconArrowDown :size="20" />
|
||||
</template>
|
||||
</NcButton>
|
||||
<div v-show="isLast || !!app.default" aria-hidden="true" class="order-selector-element__placeholder" />
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import { PropType, defineComponent } from 'vue'
|
||||
|
||||
import IconArrowDown from 'vue-material-design-icons/ArrowDown.vue'
|
||||
import IconArrowUp from 'vue-material-design-icons/ArrowUp.vue'
|
||||
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
|
||||
|
||||
interface IApp {
|
||||
id: string // app id
|
||||
icon: string // path to the icon svg
|
||||
label?: string // display name
|
||||
default?: boolean // for app as default app
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'AppOrderSelectorElement',
|
||||
components: {
|
||||
IconArrowDown,
|
||||
IconArrowUp,
|
||||
NcButton,
|
||||
},
|
||||
props: {
|
||||
app: {
|
||||
type: Object as PropType<IApp>,
|
||||
required: true,
|
||||
},
|
||||
isFirst: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isLast: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: {
|
||||
'move:up': () => true,
|
||||
'move:down': () => true,
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
t,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.order-selector-element {
|
||||
// hide default styling
|
||||
list-style: none;
|
||||
// Align children
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
// Spacing
|
||||
gap: 12px;
|
||||
padding-inline: 12px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-background-hover);
|
||||
border-radius: var(--border-radius-large);
|
||||
}
|
||||
|
||||
&--disabled {
|
||||
border-color: var(--color-text-maxcontrast);
|
||||
color: var(--color-text-maxcontrast);
|
||||
|
||||
.order-selector-element__icon {
|
||||
opacity: 75%;
|
||||
}
|
||||
}
|
||||
|
||||
&__actions {
|
||||
flex: 0 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
&__label {
|
||||
flex: 1 1;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__placeholder {
|
||||
height: 44px;
|
||||
width: 44px;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
filter: var(--background-invert-if-bright);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,122 @@
|
|||
<template>
|
||||
<NcSettingsSection :name="t('theming', 'Navigation bar settings')">
|
||||
<p>
|
||||
{{ t('theming', 'You can configure the app order used for the navigation bar. The first entry will be the default app, opened after login or when clicking on the logo.') }}
|
||||
</p>
|
||||
<NcNoteCard v-if="!!appOrder[0]?.default" type="info">
|
||||
{{ t('theming', 'The default app can not be changed because it was configured by the administrator.') }}
|
||||
</NcNoteCard>
|
||||
<NcNoteCard v-if="hasAppOrderChanged" type="info">
|
||||
{{ t('theming', 'The app order was changed, to see it in action you have to reload the page.') }}
|
||||
</NcNoteCard>
|
||||
<AppOrderSelector class="user-app-menu-order" :value.sync="appOrder" />
|
||||
</NcSettingsSection>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import { generateOcsUrl } from '@nextcloud/router'
|
||||
import { computed, defineComponent, ref } from 'vue'
|
||||
|
||||
import axios from '@nextcloud/axios'
|
||||
import AppOrderSelector from './AppOrderSelector.vue'
|
||||
import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js'
|
||||
import NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection.js'
|
||||
|
||||
/** See NavigationManager */
|
||||
interface INavigationEntry {
|
||||
/** Navigation id */
|
||||
id: string
|
||||
/** Order where this entry should be shown */
|
||||
order: number
|
||||
/** Target of the navigation entry */
|
||||
href: string
|
||||
/** The icon used for the naviation entry */
|
||||
icon: string
|
||||
/** Type of the navigation entry ('link' vs 'settings') */
|
||||
type: 'link' | 'settings'
|
||||
/** Localized name of the navigation entry */
|
||||
name: string
|
||||
/** Whether this is the default app */
|
||||
default?: boolean
|
||||
/** App that registered this navigation entry (not necessarly the same as the id) */
|
||||
app: string
|
||||
/** The key used to identify this entry in the navigations entries */
|
||||
key: number
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'UserAppMenuSection',
|
||||
components: {
|
||||
AppOrderSelector,
|
||||
NcNoteCard,
|
||||
NcSettingsSection,
|
||||
},
|
||||
setup() {
|
||||
/**
|
||||
* Track if the app order has changed, so the user can be informed to reload
|
||||
*/
|
||||
const hasAppOrderChanged = ref(false)
|
||||
|
||||
/** The enforced default app set by the administrator (if any) */
|
||||
const enforcedDefaultApp = loadState<string|null>('theming', 'enforcedDefaultApp', null)
|
||||
|
||||
/**
|
||||
* Array of all available apps, it is set by a core controller for the app menu, so it is always available
|
||||
*/
|
||||
const allApps = ref(
|
||||
Object.values(loadState<Record<string, INavigationEntry>>('core', 'apps'))
|
||||
.filter(({ type }) => type === 'link')
|
||||
.map((app) => ({ ...app, label: app.name, default: app.default && app.app === enforcedDefaultApp })),
|
||||
)
|
||||
|
||||
/**
|
||||
* Wrapper around the sortedApps list with a setter for saving any changes
|
||||
*/
|
||||
const appOrder = computed({
|
||||
get: () => allApps.value,
|
||||
set: (value) => {
|
||||
const order = {} as Record<string, Record<number, number>>
|
||||
value.forEach(({ app, key }, index) => {
|
||||
order[app] = { ...order[app], [key]: index }
|
||||
})
|
||||
|
||||
saveSetting('apporder', order)
|
||||
.then(() => {
|
||||
allApps.value = value
|
||||
hasAppOrderChanged.value = true
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn('Could not set the app order', error)
|
||||
showError(t('theming', 'Could not set the app order'))
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const saveSetting = async (key: string, value: unknown) => {
|
||||
const url = generateOcsUrl('apps/provisioning_api/api/v1/config/users/{appId}/{configKey}', {
|
||||
appId: 'core',
|
||||
configKey: key,
|
||||
})
|
||||
return await axios.post(url, {
|
||||
configValue: JSON.stringify(value),
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
appOrder,
|
||||
hasAppOrderChanged,
|
||||
|
||||
t,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.user-app-menu-order {
|
||||
margin-block: 12px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,120 @@
|
|||
<template>
|
||||
<NcSettingsSection :name="t('theming', 'Navigation bar settings')">
|
||||
<h3>{{ t('theming', 'Default app') }}</h3>
|
||||
<p class="info-note">
|
||||
{{ t('theming', 'The default app is the app that is e.g. opened after login or when the logo in the menu is clicked.') }}
|
||||
</p>
|
||||
|
||||
<NcCheckboxRadioSwitch :checked.sync="hasCustomDefaultApp" type="switch" data-cy-switch-default-app="">
|
||||
{{ t('theming', 'Use custom default app') }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
|
||||
<template v-if="hasCustomDefaultApp">
|
||||
<h4>{{ t('theming', 'Global default app') }}</h4>
|
||||
<NcSelect v-model="selectedApps"
|
||||
:close-on-select="false"
|
||||
:placeholder="t('theming', 'Global default apps')"
|
||||
:options="allApps"
|
||||
:multiple="true" />
|
||||
<h5>{{ t('theming', 'Default app priority') }}</h5>
|
||||
<p class="info-note">
|
||||
{{ t('theming', 'If an app is not enabled for a user, the next app with lower priority is used.') }}
|
||||
</p>
|
||||
<AppOrderSelector :value.sync="selectedApps" />
|
||||
</template>
|
||||
</NcSettingsSection>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { computed, defineComponent } from 'vue'
|
||||
|
||||
import axios from '@nextcloud/axios'
|
||||
|
||||
import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
|
||||
import NcSelect from '@nextcloud/vue/dist/Components/NcSelect.js'
|
||||
import NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection.js'
|
||||
import AppOrderSelector from '../AppOrderSelector.vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'AppMenuSection',
|
||||
components: {
|
||||
AppOrderSelector,
|
||||
NcCheckboxRadioSwitch,
|
||||
NcSelect,
|
||||
NcSettingsSection,
|
||||
},
|
||||
props: {
|
||||
defaultApps: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: {
|
||||
'update:defaultApps': (value: string[]) => Array.isArray(value) && value.every((id) => typeof id === 'string'),
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const hasCustomDefaultApp = computed({
|
||||
get: () => props.defaultApps.length > 0,
|
||||
set: (checked: boolean) => {
|
||||
if (checked) {
|
||||
emit('update:defaultApps', ['dashboard', 'files'])
|
||||
} else {
|
||||
selectedApps.value = []
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* All enabled apps which can be navigated
|
||||
*/
|
||||
const allApps = Object.values(
|
||||
loadState<Record<string, { id: string, name?: string, icon: string }>>('core', 'apps'),
|
||||
).map(({ id, name, icon }) => ({ label: name, id, icon }))
|
||||
|
||||
/**
|
||||
* Currently selected app, wrapps the setter
|
||||
*/
|
||||
const selectedApps = computed({
|
||||
get: () => props.defaultApps.map((id) => allApps.filter(app => app.id === id)[0]),
|
||||
set(value) {
|
||||
saveSetting('defaultApps', value.map(app => app.id))
|
||||
.then(() => emit('update:defaultApps', value.map(app => app.id)))
|
||||
.catch(() => showError(t('theming', 'Could not set global default apps')))
|
||||
},
|
||||
})
|
||||
|
||||
const saveSetting = async (key: string, value: unknown) => {
|
||||
const url = generateUrl('/apps/theming/ajax/updateAppMenu')
|
||||
return await axios.put(url, {
|
||||
setting: key,
|
||||
value,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
allApps,
|
||||
selectedApps,
|
||||
hasCustomDefaultApp,
|
||||
|
||||
t,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
h3, h4 {
|
||||
font-weight: bold;
|
||||
}
|
||||
h4, h5 {
|
||||
margin-block-start: 12px;
|
||||
}
|
||||
|
||||
.info-note {
|
||||
color: var(--color-text-maxcontrast);
|
||||
}
|
||||
</style>
|
|
@ -54,6 +54,7 @@ class PersonalTest extends TestCase {
|
|||
private ThemesService $themesService;
|
||||
private IInitialState $initialStateService;
|
||||
private ThemingDefaults $themingDefaults;
|
||||
private IAppManager $appManager;
|
||||
private Personal $admin;
|
||||
|
||||
/** @var ITheme[] */
|
||||
|
@ -65,6 +66,7 @@ class PersonalTest extends TestCase {
|
|||
$this->themesService = $this->createMock(ThemesService::class);
|
||||
$this->initialStateService = $this->createMock(IInitialState::class);
|
||||
$this->themingDefaults = $this->createMock(ThemingDefaults::class);
|
||||
$this->appManager = $this->createMock(IAppManager::class);
|
||||
|
||||
$this->initThemes();
|
||||
|
||||
|
@ -75,10 +77,12 @@ class PersonalTest extends TestCase {
|
|||
|
||||
$this->admin = new Personal(
|
||||
Application::APP_ID,
|
||||
'admin',
|
||||
$this->config,
|
||||
$this->themesService,
|
||||
$this->initialStateService,
|
||||
$this->themingDefaults,
|
||||
$this->appManager,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -112,12 +116,17 @@ class PersonalTest extends TestCase {
|
|||
->with('enforce_theme', '')
|
||||
->willReturn($enforcedTheme);
|
||||
|
||||
$this->initialStateService->expects($this->exactly(3))
|
||||
$this->appManager->expects($this->once())
|
||||
->method('getDefaultAppForUser')
|
||||
->willReturn('forcedapp');
|
||||
|
||||
$this->initialStateService->expects($this->exactly(4))
|
||||
->method('provideInitialState')
|
||||
->withConsecutive(
|
||||
['themes', $themesState],
|
||||
['enforceTheme', $enforcedTheme],
|
||||
['isUserThemingDisabled', false]
|
||||
['isUserThemingDisabled', false],
|
||||
['enforcedDefaultApp', 'forcedapp'],
|
||||
);
|
||||
|
||||
$expected = new TemplateResponse('theming', 'settings-personal');
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
*
|
||||
*/
|
||||
declare module '*.svg?raw' {
|
||||
const content: any
|
||||
const content: string
|
||||
export default content
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,212 @@
|
|||
/**
|
||||
* @copyright Copyright (c) 2023 Ferdinand Thiessen <opensource@fthiessen.de>
|
||||
*
|
||||
* @author Ferdinand Thiessen <opensource@fthiessen.de>
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
import { User } from '@nextcloud/cypress'
|
||||
|
||||
const admin = new User('admin', 'admin')
|
||||
|
||||
describe('Admin theming set default apps', () => {
|
||||
before(function() {
|
||||
// Just in case previous test failed
|
||||
cy.resetAdminTheming()
|
||||
cy.login(admin)
|
||||
})
|
||||
|
||||
it('See the current default app is the dashboard', () => {
|
||||
cy.visit('/')
|
||||
cy.url().should('match', /apps\/dashboard/)
|
||||
cy.get('#nextcloud').click()
|
||||
cy.url().should('match', /apps\/dashboard/)
|
||||
})
|
||||
|
||||
it('See the default app settings', () => {
|
||||
cy.visit('/settings/admin/theming')
|
||||
|
||||
cy.get('.settings-section').contains('Navigation bar settings').should('exist')
|
||||
cy.get('[data-cy-switch-default-app]').should('exist')
|
||||
cy.get('[data-cy-switch-default-app]').scrollIntoView()
|
||||
})
|
||||
|
||||
it('Toggle the "use custom default app" switch', () => {
|
||||
cy.get('[data-cy-switch-default-app] input').should('not.be.checked')
|
||||
cy.get('[data-cy-switch-default-app] label').click()
|
||||
cy.get('[data-cy-switch-default-app] input').should('be.checked')
|
||||
})
|
||||
|
||||
it('See the default app order selector', () => {
|
||||
cy.get('[data-cy-app-order] [data-cy-app-order-element]').each(($el, idx) => {
|
||||
if (idx === 0) cy.wrap($el).should('have.attr', 'data-cy-app-order-element', 'dashboard')
|
||||
else cy.wrap($el).should('have.attr', 'data-cy-app-order-element', 'files')
|
||||
})
|
||||
})
|
||||
|
||||
it('Change the default app', () => {
|
||||
cy.get('[data-cy-app-order] [data-cy-app-order-element="files"]').scrollIntoView()
|
||||
|
||||
cy.get('[data-cy-app-order] [data-cy-app-order-element="files"] [data-cy-app-order-button="up"]').should('be.visible')
|
||||
cy.get('[data-cy-app-order] [data-cy-app-order-element="files"] [data-cy-app-order-button="up"]').click()
|
||||
cy.get('[data-cy-app-order] [data-cy-app-order-element="files"] [data-cy-app-order-button="up"]').should('not.be.visible')
|
||||
|
||||
})
|
||||
|
||||
it('See the default app is changed', () => {
|
||||
cy.get('[data-cy-app-order] [data-cy-app-order-element]').each(($el, idx) => {
|
||||
if (idx === 0) cy.wrap($el).should('have.attr', 'data-cy-app-order-element', 'files')
|
||||
else cy.wrap($el).should('have.attr', 'data-cy-app-order-element', 'dashboard')
|
||||
})
|
||||
|
||||
cy.get('#nextcloud').click()
|
||||
cy.url().should('match', /apps\/files/)
|
||||
})
|
||||
|
||||
it('Toggle the "use custom default app" switch back to reset the default apps', () => {
|
||||
cy.visit('/settings/admin/theming')
|
||||
cy.get('[data-cy-switch-default-app]').scrollIntoView()
|
||||
|
||||
cy.get('[data-cy-switch-default-app] input').should('be.checked')
|
||||
cy.get('[data-cy-switch-default-app] label').click()
|
||||
cy.get('[data-cy-switch-default-app] input').should('be.not.checked')
|
||||
})
|
||||
|
||||
it('See the default app is changed back to default', () => {
|
||||
cy.get('#nextcloud').click()
|
||||
cy.url().should('match', /apps\/dashboard/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('User theming set app order', () => {
|
||||
before(() => {
|
||||
cy.resetAdminTheming()
|
||||
// Create random user for this test
|
||||
cy.createRandomUser().then((user) => {
|
||||
cy.login(user)
|
||||
})
|
||||
})
|
||||
|
||||
after(() => cy.logout())
|
||||
|
||||
it('See the app order settings', () => {
|
||||
cy.visit('/settings/user/theming')
|
||||
|
||||
cy.get('.settings-section').contains('Navigation bar settings').should('exist')
|
||||
cy.get('[data-cy-app-order]').scrollIntoView()
|
||||
})
|
||||
|
||||
it('See that the dashboard app is the first one', () => {
|
||||
cy.get('[data-cy-app-order] [data-cy-app-order-element]').each(($el, idx) => {
|
||||
if (idx === 0) cy.wrap($el).should('have.attr', 'data-cy-app-order-element', 'dashboard')
|
||||
else cy.wrap($el).should('have.attr', 'data-cy-app-order-element', 'files')
|
||||
})
|
||||
|
||||
cy.get('.app-menu-main .app-menu-entry').each(($el, idx) => {
|
||||
if (idx === 0) cy.wrap($el).should('have.attr', 'data-app-id', 'dashboard')
|
||||
else cy.wrap($el).should('have.attr', 'data-app-id', 'files')
|
||||
})
|
||||
})
|
||||
|
||||
it('Change the app order', () => {
|
||||
cy.get('[data-cy-app-order] [data-cy-app-order-element="files"] [data-cy-app-order-button="up"]').should('be.visible')
|
||||
cy.get('[data-cy-app-order] [data-cy-app-order-element="files"] [data-cy-app-order-button="up"]').click()
|
||||
cy.get('[data-cy-app-order] [data-cy-app-order-element="files"] [data-cy-app-order-button="up"]').should('not.be.visible')
|
||||
|
||||
cy.get('[data-cy-app-order] [data-cy-app-order-element]').each(($el, idx) => {
|
||||
if (idx === 0) cy.wrap($el).should('have.attr', 'data-cy-app-order-element', 'files')
|
||||
else cy.wrap($el).should('have.attr', 'data-cy-app-order-element', 'dashboard')
|
||||
})
|
||||
})
|
||||
|
||||
it('See the app menu order is changed', () => {
|
||||
cy.reload()
|
||||
cy.get('.app-menu-main .app-menu-entry').each(($el, idx) => {
|
||||
if (idx === 0) cy.wrap($el).should('have.attr', 'data-app-id', 'files')
|
||||
else cy.wrap($el).should('have.attr', 'data-app-id', 'dashboard')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('User theming set app order with default app', () => {
|
||||
before(() => {
|
||||
cy.resetAdminTheming()
|
||||
// install a third app
|
||||
cy.runOccCommand('app:install --force --allow-unstable calendar')
|
||||
// set calendar as default app
|
||||
cy.runOccCommand('config:system:set --value "calendar,files" defaultapp')
|
||||
|
||||
// Create random user for this test
|
||||
cy.createRandomUser().then((user) => {
|
||||
cy.login(user)
|
||||
})
|
||||
})
|
||||
|
||||
after(() => {
|
||||
cy.logout()
|
||||
cy.runOccCommand('app:remove calendar')
|
||||
})
|
||||
|
||||
it('See calendar is the default app', () => {
|
||||
cy.visit('/')
|
||||
cy.url().should('match', /apps\/calendar/)
|
||||
|
||||
cy.get('.app-menu-main .app-menu-entry').each(($el, idx) => {
|
||||
if (idx === 0) cy.wrap($el).should('have.attr', 'data-app-id', 'calendar')
|
||||
})
|
||||
})
|
||||
|
||||
it('See the app order settings: calendar is the first one', () => {
|
||||
cy.visit('/settings/user/theming')
|
||||
cy.get('[data-cy-app-order]').scrollIntoView()
|
||||
cy.get('[data-cy-app-order] [data-cy-app-order-element]').should('have.length', 3).each(($el, idx) => {
|
||||
if (idx === 0) cy.wrap($el).should('have.attr', 'data-cy-app-order-element', 'calendar')
|
||||
else if (idx === 1) cy.wrap($el).should('have.attr', 'data-cy-app-order-element', 'dashboard')
|
||||
})
|
||||
})
|
||||
|
||||
it('Can not change the default app', () => {
|
||||
cy.get('[data-cy-app-order] [data-cy-app-order-element="calendar"] [data-cy-app-order-button="up"]').should('not.be.visible')
|
||||
cy.get('[data-cy-app-order] [data-cy-app-order-element="calendar"] [data-cy-app-order-button="down"]').should('not.be.visible')
|
||||
|
||||
cy.get('[data-cy-app-order] [data-cy-app-order-element="dashboard"] [data-cy-app-order-button="up"]').should('not.be.visible')
|
||||
cy.get('[data-cy-app-order] [data-cy-app-order-element="dashboard"] [data-cy-app-order-button="down"]').should('be.visible')
|
||||
cy.get('[data-cy-app-order] [data-cy-app-order-element="files"] [data-cy-app-order-button="down"]').should('not.be.visible')
|
||||
cy.get('[data-cy-app-order] [data-cy-app-order-element="files"] [data-cy-app-order-button="up"]').should('be.visible')
|
||||
})
|
||||
|
||||
it('Change the other apps order', () => {
|
||||
cy.get('[data-cy-app-order] [data-cy-app-order-element="files"] [data-cy-app-order-button="up"]').click()
|
||||
cy.get('[data-cy-app-order] [data-cy-app-order-element="files"] [data-cy-app-order-button="up"]').should('not.be.visible')
|
||||
|
||||
cy.get('[data-cy-app-order] [data-cy-app-order-element]').each(($el, idx) => {
|
||||
if (idx === 0) cy.wrap($el).should('have.attr', 'data-cy-app-order-element', 'calendar')
|
||||
else if (idx === 1) cy.wrap($el).should('have.attr', 'data-cy-app-order-element', 'files')
|
||||
else cy.wrap($el).should('have.attr', 'data-cy-app-order-element', 'dashboard')
|
||||
})
|
||||
})
|
||||
|
||||
it('See the app menu order is changed', () => {
|
||||
cy.reload()
|
||||
cy.get('.app-menu-main .app-menu-entry').each(($el, idx) => {
|
||||
if (idx === 0) cy.wrap($el).should('have.attr', 'data-app-id', 'calendar')
|
||||
else if (idx === 1) cy.wrap($el).should('have.attr', 'data-app-id', 'files')
|
||||
else cy.wrap($el).should('have.attr', 'data-app-id', 'dashboard')
|
||||
})
|
||||
})
|
||||
})
|
|
@ -32,6 +32,7 @@
|
|||
"@nextcloud/vue": "^8.0.0-beta.8",
|
||||
"@skjnldsv/sanitize-svg": "^1.0.2",
|
||||
"@vueuse/components": "^10.4.1",
|
||||
"@vueuse/integrations": "^10.4.1",
|
||||
"autosize": "^6.0.1",
|
||||
"backbone": "^1.4.1",
|
||||
"blueimp-md5": "^2.19.0",
|
||||
|
@ -6661,6 +6662,134 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/integrations": {
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-10.5.0.tgz",
|
||||
"integrity": "sha512-fm5sXLCK0Ww3rRnzqnCQRmfjDURaI4xMsx+T+cec0ngQqHx/JgUtm8G0vRjwtonIeTBsH1Q8L3SucE+7K7upJQ==",
|
||||
"dependencies": {
|
||||
"@vueuse/core": "10.5.0",
|
||||
"@vueuse/shared": "10.5.0",
|
||||
"vue-demi": ">=0.14.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"async-validator": "*",
|
||||
"axios": "*",
|
||||
"change-case": "*",
|
||||
"drauu": "*",
|
||||
"focus-trap": "*",
|
||||
"fuse.js": "*",
|
||||
"idb-keyval": "*",
|
||||
"jwt-decode": "*",
|
||||
"nprogress": "*",
|
||||
"qrcode": "*",
|
||||
"sortablejs": "*",
|
||||
"universal-cookie": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"async-validator": {
|
||||
"optional": true
|
||||
},
|
||||
"axios": {
|
||||
"optional": true
|
||||
},
|
||||
"change-case": {
|
||||
"optional": true
|
||||
},
|
||||
"drauu": {
|
||||
"optional": true
|
||||
},
|
||||
"focus-trap": {
|
||||
"optional": true
|
||||
},
|
||||
"fuse.js": {
|
||||
"optional": true
|
||||
},
|
||||
"idb-keyval": {
|
||||
"optional": true
|
||||
},
|
||||
"jwt-decode": {
|
||||
"optional": true
|
||||
},
|
||||
"nprogress": {
|
||||
"optional": true
|
||||
},
|
||||
"qrcode": {
|
||||
"optional": true
|
||||
},
|
||||
"sortablejs": {
|
||||
"optional": true
|
||||
},
|
||||
"universal-cookie": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/integrations/node_modules/@types/web-bluetooth": {
|
||||
"version": "0.0.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.18.tgz",
|
||||
"integrity": "sha512-v/ZHEj9xh82usl8LMR3GarzFY1IrbXJw5L4QfQhokjRV91q+SelFqxQWSep1ucXEZ22+dSTwLFkXeur25sPIbw=="
|
||||
},
|
||||
"node_modules/@vueuse/integrations/node_modules/@vueuse/core": {
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.5.0.tgz",
|
||||
"integrity": "sha512-z/tI2eSvxwLRjOhDm0h/SXAjNm8N5ld6/SC/JQs6o6kpJ6Ya50LnEL8g5hoYu005i28L0zqB5L5yAl8Jl26K3A==",
|
||||
"dependencies": {
|
||||
"@types/web-bluetooth": "^0.0.18",
|
||||
"@vueuse/metadata": "10.5.0",
|
||||
"@vueuse/shared": "10.5.0",
|
||||
"vue-demi": ">=0.14.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/integrations/node_modules/@vueuse/metadata": {
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.5.0.tgz",
|
||||
"integrity": "sha512-fEbElR+MaIYyCkeM0SzWkdoMtOpIwO72x8WsZHRE7IggiOlILttqttM69AS13nrDxosnDBYdyy3C5mR1LCxHsw==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/integrations/node_modules/@vueuse/shared": {
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.5.0.tgz",
|
||||
"integrity": "sha512-18iyxbbHYLst9MqU1X1QNdMHIjks6wC7XTVf0KNOv5es/Ms6gjVFCAAWTVP2JStuGqydg3DT+ExpFORUEi9yhg==",
|
||||
"dependencies": {
|
||||
"vue-demi": ">=0.14.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/integrations/node_modules/vue-demi": {
|
||||
"version": "0.14.6",
|
||||
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.6.tgz",
|
||||
"integrity": "sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==",
|
||||
"hasInstallScript": true,
|
||||
"bin": {
|
||||
"vue-demi-fix": "bin/vue-demi-fix.js",
|
||||
"vue-demi-switch": "bin/vue-demi-switch.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vue/composition-api": "^1.0.0-rc.1",
|
||||
"vue": "^3.0.0-0 || ^2.6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vue/composition-api": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/metadata": {
|
||||
"version": "10.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.4.1.tgz",
|
||||
|
|
|
@ -59,6 +59,7 @@
|
|||
"@nextcloud/vue": "^8.0.0-beta.8",
|
||||
"@skjnldsv/sanitize-svg": "^1.0.2",
|
||||
"@vueuse/components": "^10.4.1",
|
||||
"@vueuse/integrations": "^10.4.1",
|
||||
"autosize": "^6.0.1",
|
||||
"backbone": "^1.4.1",
|
||||
"blueimp-md5": "^2.19.0",
|
||||
|
|
|
@ -609,20 +609,47 @@ class AppManagerTest extends TestCase {
|
|||
'',
|
||||
'',
|
||||
'{}',
|
||||
true,
|
||||
'files',
|
||||
],
|
||||
// none specified, without fallback
|
||||
[
|
||||
'',
|
||||
'',
|
||||
'{}',
|
||||
false,
|
||||
'',
|
||||
],
|
||||
// unexisting or inaccessible app specified, default to files
|
||||
[
|
||||
'unexist',
|
||||
'',
|
||||
'{}',
|
||||
true,
|
||||
'files',
|
||||
],
|
||||
// unexisting or inaccessible app specified, without fallbacks
|
||||
[
|
||||
'unexist',
|
||||
'',
|
||||
'{}',
|
||||
false,
|
||||
'',
|
||||
],
|
||||
// non-standard app
|
||||
[
|
||||
'settings',
|
||||
'',
|
||||
'{}',
|
||||
true,
|
||||
'settings',
|
||||
],
|
||||
// non-standard app, without fallback
|
||||
[
|
||||
'settings',
|
||||
'',
|
||||
'{}',
|
||||
false,
|
||||
'settings',
|
||||
],
|
||||
// non-standard app with fallback
|
||||
|
@ -630,13 +657,31 @@ class AppManagerTest extends TestCase {
|
|||
'unexist,settings',
|
||||
'',
|
||||
'{}',
|
||||
true,
|
||||
'settings',
|
||||
],
|
||||
// user-customized defaultapp
|
||||
[
|
||||
'',
|
||||
'files',
|
||||
'',
|
||||
true,
|
||||
'files',
|
||||
],
|
||||
// user-customized defaultapp with systemwide
|
||||
[
|
||||
'unexist,settings',
|
||||
'files',
|
||||
'',
|
||||
true,
|
||||
'files',
|
||||
],
|
||||
// user-customized defaultapp with system wide and apporder
|
||||
[
|
||||
'unexist,settings',
|
||||
'files',
|
||||
'{"settings":[1],"files":[2]}',
|
||||
true,
|
||||
'files',
|
||||
],
|
||||
// user-customized apporder fallback
|
||||
|
@ -644,15 +689,24 @@ class AppManagerTest extends TestCase {
|
|||
'',
|
||||
'',
|
||||
'{"settings":[1],"files":[2]}',
|
||||
true,
|
||||
'settings',
|
||||
],
|
||||
// user-customized apporder, but called without fallback
|
||||
[
|
||||
'',
|
||||
'',
|
||||
'{"settings":[1],"files":[2]}',
|
||||
false,
|
||||
'',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideDefaultApps
|
||||
*/
|
||||
public function testGetDefaultAppForUser($defaultApps, $userDefaultApps, $userApporder, $expectedApp) {
|
||||
public function testGetDefaultAppForUser($defaultApps, $userDefaultApps, $userApporder, $withFallbacks, $expectedApp) {
|
||||
$user = $this->newUser('user1');
|
||||
|
||||
$this->userSession->expects($this->once())
|
||||
|
@ -671,6 +725,6 @@ class AppManagerTest extends TestCase {
|
|||
['user1', 'core', 'apporder', '[]', $userApporder],
|
||||
]);
|
||||
|
||||
$this->assertEquals($expectedApp, $this->manager->getDefaultAppForUser());
|
||||
$this->assertEquals($expectedApp, $this->manager->getDefaultAppForUser(null, $withFallbacks));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue