Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| beacfdaeae |
@@ -1,10 +1,5 @@
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- fix(config): store the app store category cache lazily with only the names in the languages in use, refresh it weekly and when the settings are saved, and load it only when categories are used
|
||||
- fix(admin): save all settings in one request so changes are not lost after the page reloads
|
||||
|
||||
## 6.2.0
|
||||
|
||||
### Added
|
||||
|
||||
@@ -49,8 +49,6 @@ Users can disable the menu using the page of personal settings.
|
||||
|
||||
Use the shortcut `Ctrl`+`o` to open and to hide the side menu. Use `tab` to navigate.
|
||||
|
||||
The names of the app store categories are cached for a week, only in the languages that are in use on the instance (and English). If a user switches to a new language and sees English category names, save the settings on the administration page to refresh the cache right away.
|
||||
|
||||
### Use first top menu app as default app
|
||||
|
||||
You can easily let Custom Menu redirect to the first app in the top menu by changing the following parameter in your `config/config.php`:
|
||||
|
||||
@@ -12,15 +12,12 @@ use OCA\SideMenu\Service\AppRepository;
|
||||
use OCA\SideMenu\Service\CategoryRepository;
|
||||
use OCA\SideMenu\Service\Color;
|
||||
use OCA\SideMenu\Service\ConfigProxy;
|
||||
use OCA\SideMenu\Service\LangRepository;
|
||||
use OCA\Theming\ThemingDefaults;
|
||||
use OCP\AppFramework\App;
|
||||
use OCP\AppFramework\Bootstrap\IBootContext;
|
||||
use OCP\AppFramework\Bootstrap\IBootstrap;
|
||||
use OCP\AppFramework\Bootstrap\IRegistrationContext;
|
||||
use OCP\AppFramework\Utility\ITimeFactory;
|
||||
use OCP\EventDispatcher\IEventDispatcher;
|
||||
use OCP\IAppConfig;
|
||||
use OCP\IConfig;
|
||||
use OCP\INavigationManager;
|
||||
use OCP\IRequest;
|
||||
@@ -59,10 +56,7 @@ class Application extends App implements IBootstrap
|
||||
$c->get(ConfigProxy::class),
|
||||
$c->get(IConfig::class),
|
||||
$c->get(IFactory::class),
|
||||
$c->get(IUserSession::class),
|
||||
$c->get(IAppConfig::class),
|
||||
$c->get(ITimeFactory::class),
|
||||
$c->get(LangRepository::class)
|
||||
$c->get(IUserSession::class)
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -20,14 +20,12 @@
|
||||
namespace OCA\SideMenu\Controller;
|
||||
|
||||
use OCA\SideMenu\AppInfo\Application;
|
||||
use OCA\SideMenu\Service\CategoryRepository;
|
||||
use OCA\SideMenu\Service\Color;
|
||||
use OCA\SideMenu\Service\ConfigProxy;
|
||||
use OCA\SideMenu\Service\LangRepository;
|
||||
use OCP\AppFramework\Controller;
|
||||
use OCP\AppFramework\Http\Attribute\FrontpageRoute;
|
||||
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
|
||||
use OCP\AppFramework\Http\Attribute\PasswordConfirmationRequired;
|
||||
use OCP\AppFramework\Http\DataDownloadResponse;
|
||||
use OCP\AppFramework\Http\JSONResponse;
|
||||
use OCP\AppFramework\Http\RedirectResponse;
|
||||
@@ -37,12 +35,6 @@ use OCP\IURLGenerator;
|
||||
|
||||
class AdminSettingController extends Controller
|
||||
{
|
||||
/**
|
||||
* Keys that are managed by the app or by Nextcloud itself ("enabled")
|
||||
* and must not be written from the settings form.
|
||||
*/
|
||||
protected const UNSAVED_KEYS = ['cache', 'cache-categories', 'enabled'];
|
||||
|
||||
public function __construct(
|
||||
$appName,
|
||||
IRequest $request,
|
||||
@@ -51,7 +43,6 @@ class AdminSettingController extends Controller
|
||||
protected IURLGenerator $urlGenerator,
|
||||
protected Color $color,
|
||||
protected LangRepository $langRepository,
|
||||
protected CategoryRepository $categoryRepository,
|
||||
) {
|
||||
parent::__construct($appName, $request);
|
||||
}
|
||||
@@ -60,7 +51,7 @@ class AdminSettingController extends Controller
|
||||
#[FrontpageRoute(verb: 'GET', url: '/admin/cache/remove')]
|
||||
public function removeCache(): RedirectResponse
|
||||
{
|
||||
$this->categoryRepository->clearCache();
|
||||
$this->config->setAppValue(Application::APP_ID, 'cache-categories', '[]');
|
||||
|
||||
return new RedirectResponse($this->urlGenerator->linkToRoute('settings.AdminSettings.index', [
|
||||
'section' => Application::APP_ID,
|
||||
@@ -140,77 +131,7 @@ class AdminSettingController extends Controller
|
||||
'top-menu-mouse-over-hidden-label',
|
||||
];
|
||||
|
||||
$defaults = $this->getDefaults();
|
||||
|
||||
$config = [];
|
||||
|
||||
foreach ($keys as $key) {
|
||||
if (!isset($defaults[$key])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (in_array($key, $booleans)) {
|
||||
$config[$key] = $this->configProxy->getAppValueBool($key, $defaults[$key]);
|
||||
} elseif (in_array($key, $arrays)) {
|
||||
$config[$key] = $this->configProxy->getAppValueArray($key, $defaults[$key]);
|
||||
} elseif (in_array($key, $integers)) {
|
||||
$config[$key] = $this->configProxy->getAppValueInt($key, $defaults[$key]);
|
||||
} else {
|
||||
$config[$key] = $this->configProxy->getAppValue($key, $defaults[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($defaults as $key => $default) {
|
||||
if (array_key_exists($key, $config)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (in_array($key, $booleans)) {
|
||||
$config[$key] = $default === '1';
|
||||
} elseif (in_array($key, $arrays)) {
|
||||
$config[$key] = json_decode($default);
|
||||
} elseif (in_array($key, $integers)) {
|
||||
$config[$key] = (int) $default;
|
||||
} else {
|
||||
$config[$key] = $default;
|
||||
}
|
||||
}
|
||||
|
||||
$config['langs'] = $this->langRepository->getUsedLangs();
|
||||
|
||||
return new JSONResponse($config);
|
||||
}
|
||||
|
||||
#[PasswordConfirmationRequired]
|
||||
#[FrontpageRoute(verb: 'POST', url: '/admin/config')]
|
||||
public function saveConfiguration(array $config): JSONResponse
|
||||
{
|
||||
$allowed = array_diff(array_keys($this->getDefaults()), self::UNSAVED_KEYS);
|
||||
|
||||
foreach ($config as $key => $value) {
|
||||
if (!in_array($key, $allowed, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_array($value)) {
|
||||
$value = json_encode($value);
|
||||
} elseif (is_bool($value)) {
|
||||
$value = $value ? '1' : '0';
|
||||
}
|
||||
|
||||
$this->config->setAppValue(Application::APP_ID, $key, (string) $value);
|
||||
}
|
||||
|
||||
// New stylesheet URL, and category names that match the new settings
|
||||
$this->config->setAppValue(Application::APP_ID, 'cache', (string) time());
|
||||
$this->categoryRepository->clearCache();
|
||||
|
||||
return new JSONResponse(['status' => 'ok']);
|
||||
}
|
||||
|
||||
protected function getDefaults(): array
|
||||
{
|
||||
return [
|
||||
$defaults = [
|
||||
'opener-only' => '0',
|
||||
'opener-hover' => '0',
|
||||
'display-logo' => '1',
|
||||
@@ -267,5 +188,43 @@ class AdminSettingController extends Controller
|
||||
|
||||
'categories-order-type' => 'default',
|
||||
];
|
||||
|
||||
$config = [];
|
||||
|
||||
foreach ($keys as $key) {
|
||||
if (!isset($defaults[$key])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (in_array($key, $booleans)) {
|
||||
$config[$key] = $this->configProxy->getAppValueBool($key, $defaults[$key]);
|
||||
} elseif (in_array($key, $arrays)) {
|
||||
$config[$key] = $this->configProxy->getAppValueArray($key, $defaults[$key]);
|
||||
} elseif (in_array($key, $integers)) {
|
||||
$config[$key] = $this->configProxy->getAppValueInt($key, $defaults[$key]);
|
||||
} else {
|
||||
$config[$key] = $this->configProxy->getAppValue($key, $defaults[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($defaults as $key => $default) {
|
||||
if (array_key_exists($key, $config)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (in_array($key, $booleans)) {
|
||||
$config[$key] = $default === '1';
|
||||
} elseif (in_array($key, $arrays)) {
|
||||
$config[$key] = json_decode($default);
|
||||
} elseif (in_array($key, $integers)) {
|
||||
$config[$key] = (int) $default;
|
||||
} else {
|
||||
$config[$key] = $default;
|
||||
}
|
||||
}
|
||||
|
||||
$config['langs'] = $this->langRepository->getUsedLangs();
|
||||
|
||||
return new JSONResponse($config);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,8 +40,7 @@ class AppRepository
|
||||
{
|
||||
$navigation = $this->navigationManager->getAll();
|
||||
$appCategoriesCustom = $this->config->getAppValueArray('apps-categories-custom', '[]');
|
||||
// Only needed to check custom categories, skip loading them otherwise
|
||||
$categories = empty(array_filter($appCategoriesCustom)) ? [] : $this->categoryRepository->getOrderedCategories();
|
||||
$categories = $this->categoryRepository->getOrderedCategories();
|
||||
$apps = $this->ocApp->listAllApps();
|
||||
$visibleApps = [];
|
||||
|
||||
|
||||
@@ -4,8 +4,6 @@ namespace OCA\SideMenu\Service;
|
||||
|
||||
use OC\App\AppStore\Fetcher\CategoryFetcher;
|
||||
use OCA\SideMenu\AppInfo\Application;
|
||||
use OCP\AppFramework\Utility\ITimeFactory;
|
||||
use OCP\IAppConfig;
|
||||
use OCP\IConfig;
|
||||
use OCP\IUserSession;
|
||||
use OCP\L10N\IFactory;
|
||||
@@ -17,22 +15,12 @@ use OCP\L10N\IFactory;
|
||||
*/
|
||||
class CategoryRepository
|
||||
{
|
||||
public const CACHE_KEY = 'cache-categories';
|
||||
|
||||
/**
|
||||
* The app store categories rarely change, refresh them once a week.
|
||||
*/
|
||||
protected const CACHE_TTL = 604800;
|
||||
|
||||
public function __construct(
|
||||
protected CategoryFetcher $categoryFetcher,
|
||||
protected ConfigProxy $config,
|
||||
protected IConfig $iConfig,
|
||||
protected IFactory $l10nFactory,
|
||||
protected IUserSession $userSession,
|
||||
protected IAppConfig $appConfig,
|
||||
protected ITimeFactory $timeFactory,
|
||||
protected LangRepository $langRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -44,9 +32,15 @@ class CategoryRepository
|
||||
$currentLanguage = substr($this->l10nFactory->findLanguage(), 0, 2);
|
||||
$type = $this->config->getAppValue('categories-order-type', 'default');
|
||||
$order = $this->config->getAppValueArray('categories-order', '[]');
|
||||
$categoriesLabels = $this->getCategories();
|
||||
$categoriesLabels = $this->config->getAppValueArray('cache-categories', '[]');
|
||||
$customCategories = $this->config->getAppValueArray('categories-custom', '[]');
|
||||
|
||||
if (empty($categoriesLabels)) {
|
||||
$categoriesLabels = $this->categoryFetcher->get();
|
||||
|
||||
$this->iConfig->setAppValue(Application::APP_ID, 'cache-categories', json_encode($categoriesLabels));
|
||||
}
|
||||
|
||||
foreach ($categoriesLabels as $k => $category) {
|
||||
$categoriesLabels[$category['id']] = $category['translations'][$currentLanguage]['name'] ??
|
||||
$category['translations']['en']['name'];
|
||||
@@ -91,71 +85,4 @@ class CategoryRepository
|
||||
|
||||
return $categoriesLabels;
|
||||
}
|
||||
|
||||
public function clearCache(): void
|
||||
{
|
||||
$this->appConfig->deleteKey(Application::APP_ID, self::CACHE_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* The languages to keep in the cache: English as fallback, the
|
||||
* default language and the languages of the users, as two-letter
|
||||
* codes like getOrderedCategories() looks them up. A user who picks
|
||||
* a new language sees English names until the cache is refreshed,
|
||||
* which saving the settings does right away.
|
||||
*/
|
||||
protected function getCachedLangs(): array
|
||||
{
|
||||
$langs = $this->langRepository->getUsedLangs();
|
||||
$langs[] = (string) $this->iConfig->getSystemValue('default_language', 'en');
|
||||
$langs[] = (string) $this->iConfig->getSystemValue('force_language', 'en');
|
||||
|
||||
return array_values(array_unique(array_filter(array_map(
|
||||
static fn (string $lang) => strtolower(substr($lang, 0, 2)),
|
||||
array_merge(['en'], $langs)
|
||||
))));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the app store categories with only their names, from a
|
||||
* cache that is stored lazily: it holds the names in every language,
|
||||
* so it must not be loaded on every request with the other settings.
|
||||
*/
|
||||
protected function getCategories(): array
|
||||
{
|
||||
$cache = $this->appConfig->getValueArray(Application::APP_ID, self::CACHE_KEY, [], lazy: true);
|
||||
|
||||
if (isset($cache['expires'], $cache['categories']) && $cache['expires'] > $this->timeFactory->getTime()) {
|
||||
return $cache['categories'];
|
||||
}
|
||||
|
||||
$langs = $this->getCachedLangs();
|
||||
$categories = [];
|
||||
|
||||
foreach ($this->categoryFetcher->get() as $category) {
|
||||
$names = [];
|
||||
|
||||
foreach ($category['translations'] ?? [] as $lang => $translation) {
|
||||
if (in_array($lang, $langs, true)) {
|
||||
$names[$lang] = ['name' => $translation['name'] ?? ''];
|
||||
}
|
||||
}
|
||||
|
||||
$categories[] = [
|
||||
'id' => $category['id'],
|
||||
'translations' => $names,
|
||||
];
|
||||
}
|
||||
|
||||
// Don't cache an empty list, e.g. when the app store is unreachable
|
||||
if (!empty($categories)) {
|
||||
$this->clearCache();
|
||||
$this->appConfig->setValueArray(Application::APP_ID, self::CACHE_KEY, [
|
||||
'expires' => $this->timeFactory->getTime() + self::CACHE_TTL,
|
||||
'categories' => $categories,
|
||||
], lazy: true);
|
||||
}
|
||||
|
||||
return $categories;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,9 +20,6 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import axios from '@nextcloud/axios'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { NcButton, NcLoadingIcon } from '@nextcloud/vue'
|
||||
import { ref } from 'vue'
|
||||
import { waitPasswordConfirmation } from '../../lib/setting.js'
|
||||
@@ -49,28 +46,52 @@ const filterConfig = (value) => {
|
||||
}
|
||||
|
||||
const save = async () => {
|
||||
const data = filterConfig(config)
|
||||
const size = Object.keys(data).length
|
||||
let counter = 0
|
||||
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
await waitPasswordConfirmation()
|
||||
} catch {
|
||||
loading.value = false
|
||||
return
|
||||
const update = () => {
|
||||
++counter
|
||||
|
||||
if (counter === size) {
|
||||
loading.value = false
|
||||
|
||||
if (!error.value) {
|
||||
location.reload()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// One request for all settings: separate requests per setting could
|
||||
// leave a half-saved configuration in Nextcloud's config cache when
|
||||
// the page reloads.
|
||||
await axios.post(generateUrl('/apps/side_menu/admin/config'), {
|
||||
config: filterConfig(config),
|
||||
waitPasswordConfirmation()
|
||||
.then(() => {
|
||||
for (let key in data) {
|
||||
let value = data[key]
|
||||
|
||||
if (Array.isArray(value) || typeof value === 'object') {
|
||||
value = JSON.stringify(value)
|
||||
} else if (typeof value === 'boolean') {
|
||||
value = value ? '1' : '0'
|
||||
}
|
||||
|
||||
OCP.AppConfig.setValue('side_menu', key, value.toString(), {
|
||||
success() {
|
||||
update()
|
||||
},
|
||||
error() {
|
||||
error.value = `Error while saving ${key}`
|
||||
update()
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
counter = 0
|
||||
loading.value = false
|
||||
error.value = null
|
||||
})
|
||||
location.reload()
|
||||
} catch {
|
||||
error.value = t('side_menu', 'Error while saving the settings')
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -153,7 +153,7 @@ const appLimit = () => {
|
||||
})
|
||||
}
|
||||
|
||||
return Math.max(0, Math.floor(maxWidth / 60))
|
||||
return Math.max(0, Math.floor(maxWidth / 60) - 1)
|
||||
}
|
||||
|
||||
const makeStyle = (app) => {
|
||||
|
||||
Reference in New Issue
Block a user