mirror of https://github.com/nextcloud/server.git
feat(files): add view config service to store user-config per view
Signed-off-by: John Molakvoæ <skjnldsv@protonmail.com>pull/37731/head
parent
ff58cd5227
commit
d7ab8da1ef
@ -0,0 +1,184 @@
|
||||
<?php
|
||||
/**
|
||||
* @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* 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/>.
|
||||
*
|
||||
*/
|
||||
namespace OCA\Files\Service;
|
||||
|
||||
use OCA\Files\AppInfo\Application;
|
||||
use OCP\IConfig;
|
||||
use OCP\IUser;
|
||||
use OCP\IUserSession;
|
||||
|
||||
class ViewConfig {
|
||||
const CONFIG_KEY = 'files_views_configs';
|
||||
const ALLOWED_CONFIGS = [
|
||||
[
|
||||
// The default sorting key for the files list view
|
||||
'key' => 'sorting_mode',
|
||||
// null by default as views can provide default sorting key
|
||||
// and will fallback to it if user hasn't change it
|
||||
'default' => null,
|
||||
],
|
||||
[
|
||||
// The default sorting direction for the files list view
|
||||
'key' => 'sorting_direction',
|
||||
'default' => 'asc',
|
||||
'allowed' => ['asc', 'desc'],
|
||||
],
|
||||
[
|
||||
// If the navigation entry for this view is expanded or not
|
||||
'key' => 'expanded',
|
||||
'default' => true,
|
||||
'allowed' => [true, false],
|
||||
],
|
||||
];
|
||||
|
||||
protected IConfig $config;
|
||||
protected ?IUser $user = null;
|
||||
|
||||
public function __construct(IConfig $config, IUserSession $userSession) {
|
||||
$this->config = $config;
|
||||
$this->user = $userSession->getUser();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of all allowed user config keys
|
||||
* @return string[]
|
||||
*/
|
||||
public function getAllowedConfigKeys(): array {
|
||||
return array_map(function($config) {
|
||||
return $config['key'];
|
||||
}, self::ALLOWED_CONFIGS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of allowed config values for a given key
|
||||
*
|
||||
* @param string $key a valid config key
|
||||
* @return array
|
||||
*/
|
||||
private function getAllowedConfigValues(string $key): array {
|
||||
foreach (self::ALLOWED_CONFIGS as $config) {
|
||||
if ($config['key'] === $key) {
|
||||
return $config['allowed'] ?? [];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default config value for a given key
|
||||
*
|
||||
* @param string $key a valid config key
|
||||
* @return string|bool|null
|
||||
*/
|
||||
private function getDefaultConfigValue(string $key) {
|
||||
foreach (self::ALLOWED_CONFIGS as $config) {
|
||||
if ($config['key'] === $key) {
|
||||
return $config['default'];
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a user config
|
||||
*
|
||||
* @param string $view
|
||||
* @param string $key
|
||||
* @param string|bool $value
|
||||
* @throws \Exception
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function setConfig(string $view, string $key, $value): void {
|
||||
if ($this->user === null) {
|
||||
throw new \Exception('No user logged in');
|
||||
}
|
||||
|
||||
if (!$view) {
|
||||
throw new \Exception('Unknown view');
|
||||
}
|
||||
|
||||
if (!in_array($key, $this->getAllowedConfigKeys())) {
|
||||
throw new \InvalidArgumentException('Unknown config key');
|
||||
}
|
||||
|
||||
if (!in_array($value, $this->getAllowedConfigValues($key))
|
||||
&& !empty($this->getAllowedConfigValues($key))) {
|
||||
throw new \InvalidArgumentException('Invalid config value');
|
||||
}
|
||||
|
||||
// Cast boolean values
|
||||
if (is_bool($this->getDefaultConfigValue($key))) {
|
||||
$value = $value === '1';
|
||||
}
|
||||
|
||||
$config = $this->getConfigs();
|
||||
$config[$view][$key] = $value;
|
||||
|
||||
$this->config->setUserValue($this->user->getUID(), Application::APP_ID, self::CONFIG_KEY, json_encode($config));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current user configs array for a given view
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getConfig(string $view): array {
|
||||
if ($this->user === null) {
|
||||
throw new \Exception('No user logged in');
|
||||
}
|
||||
|
||||
$userId = $this->user->getUID();
|
||||
$configs = json_decode($this->config->getUserValue($userId, Application::APP_ID, self::CONFIG_KEY, '[]'), true);
|
||||
|
||||
if (!isset($configs[$view])) {
|
||||
$configs[$view] = [];
|
||||
}
|
||||
|
||||
// Extend undefined values with defaults
|
||||
return array_reduce(self::ALLOWED_CONFIGS, function($carry, $config) use ($view, $configs) {
|
||||
$key = $config['key'];
|
||||
$carry[$key] = $configs[$view][$key] ?? $this->getDefaultConfigValue($key);
|
||||
return $carry;
|
||||
}, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current user configs array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getConfigs(): array {
|
||||
if ($this->user === null) {
|
||||
throw new \Exception('No user logged in');
|
||||
}
|
||||
|
||||
$userId = $this->user->getUID();
|
||||
$configs = json_decode($this->config->getUserValue($userId, Application::APP_ID, self::CONFIG_KEY, '[]'), true);
|
||||
$views = array_keys($configs);
|
||||
|
||||
return array_reduce($views, function($carry, $view) use ($configs) {
|
||||
$carry[$view] = $this->getConfig($view);
|
||||
return $carry;
|
||||
}, []);
|
||||
}
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
/**
|
||||
* @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @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 Vue from 'vue'
|
||||
|
||||
import { useViewConfigStore } from '../store/viewConfig'
|
||||
import type { Navigation } from '../services/Navigation'
|
||||
|
||||
export default Vue.extend({
|
||||
setup() {
|
||||
const viewConfigStore = useViewConfigStore()
|
||||
return {
|
||||
viewConfigStore,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
currentView(): Navigation {
|
||||
return this.$navigation.active
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the sorting mode for the current view
|
||||
*/
|
||||
sortingMode(): string {
|
||||
return this.viewConfigStore.getConfig(this.currentView.id)?.sorting_mode
|
||||
|| this.currentView?.defaultSortKey
|
||||
|| 'basename'
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the sorting direction for the current view
|
||||
*/
|
||||
isAscSorting(): boolean {
|
||||
const sortingDirection = this.viewConfigStore.getConfig(this.currentView.id)?.sorting_direction
|
||||
return sortingDirection === 'asc'
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggleSortBy(key: string) {
|
||||
// If we're already sorting by this key, flip the direction
|
||||
if (this.sortingMode === key) {
|
||||
this.viewConfigStore.toggleSortingDirection(this.currentView.id)
|
||||
return
|
||||
}
|
||||
// else sort ASC by this new key
|
||||
this.viewConfigStore.setSortingBy(key, this.currentView.id)
|
||||
},
|
||||
},
|
||||
})
|
@ -1,80 +0,0 @@
|
||||
/**
|
||||
* @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
/* eslint-disable */
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { defineStore } from 'pinia'
|
||||
import Vue from 'vue'
|
||||
import axios from '@nextcloud/axios'
|
||||
import type { direction, SortingStore } from '../types.ts'
|
||||
|
||||
const saveUserConfig = (mode: string, direction: direction, view: string) => {
|
||||
return axios.post(generateUrl('/apps/files/api/v1/sorting'), {
|
||||
mode,
|
||||
direction,
|
||||
view,
|
||||
})
|
||||
}
|
||||
|
||||
const filesSortingConfig = loadState('files', 'filesSortingConfig', {}) as SortingStore
|
||||
|
||||
export const useSortingStore = defineStore('sorting', {
|
||||
state: () => ({
|
||||
filesSortingConfig,
|
||||
}),
|
||||
|
||||
getters: {
|
||||
isAscSorting: (state) => (view: string = 'files') => state.filesSortingConfig[view]?.direction !== 'desc',
|
||||
getSortingMode: (state) => (view: string = 'files') => state.filesSortingConfig[view]?.mode,
|
||||
},
|
||||
|
||||
actions: {
|
||||
/**
|
||||
* Set the sorting key AND sort by ASC
|
||||
* The key param must be a valid key of a File object
|
||||
* If not found, will be searched within the File attributes
|
||||
*/
|
||||
setSortingBy(key: string = 'basename', view: string = 'files') {
|
||||
const config = this.filesSortingConfig[view] || {}
|
||||
config.mode = key
|
||||
config.direction = 'asc'
|
||||
|
||||
// Save new config
|
||||
Vue.set(this.filesSortingConfig, view, config)
|
||||
saveUserConfig(config.mode, config.direction, view)
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle the sorting direction
|
||||
*/
|
||||
toggleSortingDirection(view: string = 'files') {
|
||||
const config = this.filesSortingConfig[view] || { 'direction': 'asc' }
|
||||
const newDirection = config.direction === 'asc' ? 'desc' : 'asc'
|
||||
config.direction = newDirection
|
||||
|
||||
// Save new config
|
||||
Vue.set(this.filesSortingConfig, view, config)
|
||||
saveUserConfig(config.mode, config.direction, view)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -0,0 +1,103 @@
|
||||
/**
|
||||
* @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
/* eslint-disable */
|
||||
import { defineStore } from 'pinia'
|
||||
import { emit, subscribe } from '@nextcloud/event-bus'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import axios from '@nextcloud/axios'
|
||||
import Vue from 'vue'
|
||||
|
||||
import { ViewConfigs, ViewConfigStore, ViewId } from '../types.ts'
|
||||
import { ViewConfig } from '../types'
|
||||
|
||||
const viewConfig = loadState('files', 'viewConfigs', {}) as ViewConfigs
|
||||
|
||||
export const useViewConfigStore = () => {
|
||||
const store = defineStore('viewconfig', {
|
||||
state: () => ({
|
||||
viewConfig,
|
||||
} as ViewConfigStore),
|
||||
|
||||
getters: {
|
||||
getConfig: (state) => (view: ViewId): ViewConfig => state.viewConfig[view] || {},
|
||||
},
|
||||
|
||||
actions: {
|
||||
/**
|
||||
* Update the view config local store
|
||||
*/
|
||||
onUpdate(view: ViewId, key: string, value: boolean) {
|
||||
if (!this.viewConfig[view]) {
|
||||
Vue.set(this.viewConfig, view, {})
|
||||
}
|
||||
Vue.set(this.viewConfig[view], key, value)
|
||||
},
|
||||
|
||||
/**
|
||||
* Update the view config local store AND on server side
|
||||
*/
|
||||
async update(view: ViewId, key: string, value: boolean) {
|
||||
axios.put(generateUrl(`/apps/files/api/v1/views/${view}/${key}`), {
|
||||
value,
|
||||
})
|
||||
|
||||
emit('files:viewconfig:updated', { view, key, value })
|
||||
},
|
||||
|
||||
/**
|
||||
* Set the sorting key AND sort by ASC
|
||||
* The key param must be a valid key of a File object
|
||||
* If not found, will be searched within the File attributes
|
||||
*/
|
||||
setSortingBy(key: string = 'basename', view: string = 'files') {
|
||||
// Save new config
|
||||
this.update(view, 'sorting_mode', key)
|
||||
this.update(view, 'sorting_direction', 'asc')
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle the sorting direction
|
||||
*/
|
||||
toggleSortingDirection(view: string = 'files') {
|
||||
const config = this.getConfig(view) || { 'sorting_direction': 'asc' }
|
||||
const newDirection = config.sorting_direction === 'asc' ? 'desc' : 'asc'
|
||||
|
||||
// Save new config
|
||||
this.update(view, 'sorting_direction', newDirection)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const viewConfigStore = store()
|
||||
|
||||
// Make sure we only register the listeners once
|
||||
if (!viewConfigStore._initialized) {
|
||||
subscribe('files:viewconfig:updated', function({ view, key, value }: { view: ViewId, key: string, value: boolean }) {
|
||||
viewConfigStore.onUpdate(view, key, value)
|
||||
})
|
||||
viewConfigStore._initialized = true
|
||||
}
|
||||
|
||||
return viewConfigStore
|
||||
}
|
||||
|
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
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue