add categories customization

This commit is contained in:
Simon Vieille 2025-04-13 15:17:56 +02:00
commit 89ca510897
Signed by: deblan
GPG key ID: 579388D585F70417
5 changed files with 354 additions and 216 deletions

View file

@ -20,7 +20,9 @@
namespace OCA\SideMenu\Controller;
use OCA\SideMenu\AppInfo\Application;
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;
@ -30,7 +32,6 @@ use OCP\AppFramework\Http\RedirectResponse;
use OCP\IConfig;
use OCP\IRequest;
use OCP\IURLGenerator;
use OCA\SideMenu\Service\Color;
class AdminSettingController extends Controller
{
@ -41,6 +42,7 @@ class AdminSettingController extends Controller
protected ConfigProxy $configProxy,
protected IURLGenerator $urlGenerator,
protected Color $color,
protected LangRepository $langRepository,
) {
parent::__construct($appName, $request);
}
@ -65,6 +67,7 @@ class AdminSettingController extends Controller
$excludedKeys = [
'cache',
'cache-categories',
'langs',
];
foreach ($keys as $key) {
@ -204,6 +207,8 @@ class AdminSettingController extends Controller
}
}
$config['langs'] = $this->langRepository->getUsedLangs();
return new JSONResponse($config);
}
}

View file

@ -1,214 +0,0 @@
<!--
@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/>.
-->
<template>
<div>
<ul
class="side-menu-setting-list"
:class="{ hide: values.length === 0 }"
>
<li
v-for="(item, key) in values"
:key="key"
class="side-menu-setting-list-item"
@click="showEditForm(item)"
>
{{ item.en }}
</li>
</ul>
<NcActions>
<NcActionButton
icon="icon-add"
@click="showAddForm"
></NcActionButton>
</NcActions>
<NcModal
v-if="addForm"
@close="hideAddForm"
>
<div class="modal__content">
<div
v-for="(lang, key) in langs"
:key="key"
>
<span class="lang">{{ lang }}</span>
<input
v-model="newValue[lang]"
type="text"
required
style="width: calc(100% - 100px)"
/>
</div>
<NcActions>
<NcActionButton
icon="icon-checkmark"
@click="saveAdd"
></NcActionButton>
</NcActions>
</div>
</NcModal>
<NcModal
v-if="editForm"
@close="hideEditForm"
>
<div class="modal__content">
<div
v-for="(lang, key) in langs"
:key="key"
>
<span class="lang">{{ lang }}</span>
<input
v-model="editValue[lang]"
type="text"
required
style="width: calc(100% - 100px)"
/>
</div>
<div class="pull-right">
<NcActions>
<NcActionButton
icon="icon-delete"
@click="removeEdit"
></NcActionButton>
</NcActions>
</div>
<NcActions>
<NcActionButton
icon="icon-checkmark"
@click="saveEdit"
></NcActionButton>
</NcActions>
</div>
</NcModal>
</div>
</template>
<script setup>
import { NcModal, NcActions, NcActionButton } from '@nextcloud/vue'
import { ref } from 'vue'
const { langs, values } = defineProps(['lang', 'values'])
const input = ref(null)
const values = ref([])
const langs = ref([])
const addForm = ref(false)
const editForm = ref(false)
const newValue = ref({})
const editValue = ref({})
// mounted() {
// this.input = document.querySelector('input[name="categories-custom"]')
// this.init()
// },
// methods: {
// init() {
// this.values = JSON.parse(this.input.value)
// this.langs = JSON.parse(this.input.getAttribute('data-langs'))
// },
// update() {
// this.input.value = JSON.stringify(this.values)
// },
// showAddForm() {
// this.newValue = { id: 'cat' + Math.random().toString().replace('0.', '') }
//
// this.addForm = true
// },
// showEditForm(value) {
// this.editValue = { id: value.id }
//
// for (let i of this.langs) {
// this.editValue[i] = typeof value[i] !== 'undefined' ? value[i] : ''
// }
//
// this.editForm = true
// },
// saveAdd() {
// for (let i of this.langs) {
// if (!this.newValue[i] || /^\s*$/.test(this.newValue[i])) {
// return
// }
// }
//
// this.values.push(this.newValue)
// this.update()
// this.hideAddForm()
// this.newValue = {}
// },
// saveEdit() {
// for (let i of this.langs) {
// if (!this.editValue[i] || /^\s*$/.test(this.editValue[i])) {
// return
// }
// }
//
// for (let i in this.values) {
// if (this.values[i].id === this.editValue.id) {
// this.values[i] = this.editValue
// }
// }
//
// this.update()
// this.hideEditForm()
// },
// removeEdit() {
// for (let i in this.values) {
// if (this.values[i].id === this.editValue.id) {
// this.values.splice(i, 1)
// }
// }
//
// this.update()
// this.hideEditForm()
// },
// hideAddForm() {
// this.addForm = false
// },
// hideEditForm() {
// this.editForm = false
// },
// },
// }
</script>
<style>
.modal__content {
padding: 10px;
}
.modal__content .lang {
width: 60px;
display: inline-block;
padding: 4px;
box-sizing: border-box;
}
.pull-right {
float: right;
}
</style>
<style scoped>
.hide {
display: none;
}
</style>

View file

@ -0,0 +1,322 @@
<!--
@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/>.
-->
<template>
<NcButton
aria-label="t('side_menu', 'Customize')"
variant="primary"
@click="openModal"
>
{{ t('side_menu', 'Customize') }}
</NcButton>
<NcModal
v-if="modal"
_size="small"
@close="closeModal"
>
<div class="modal__content">
<div class="menu">
<NcButton
aria-label="t('side_menu', 'Categories')"
:variant="section === 'cats' ? 'primary' : 'secondary'"
@click="setSection('cats')"
>
{{ t('side_menu', 'Categories') }}
</NcButton>
<NcButton
aria-label="t('side_menu', 'Applications')"
:variant="section === 'apps' ? 'primary' : 'secondary'"
@click="setSection('apps')"
>
{{ t('side_menu', 'Applications') }}
</NcButton>
</div>
<div v-if="section === 'cats'">
<table width="100%" v-if="!newCustomCategory && editCustomCategoryKey === null">
<tbody>
<tr v-for="(item, key) in categoriesCustom" :key="key">
<td>{{ item[langs[0]] }}</td>
<td width="50px">
<NcActions>
<NcActionButton
icon="icon-edit"
@click="editCustomCategory(key)"
></NcActionButton>
<NcActionButton
icon="icon-delete"
@click="removeCustomCategory(key)"
></NcActionButton>
</NcActions>
</td>
</tr>
</tbody>
</table>
<div v-else class="form">
<template v-if="newCustomCategory">
<NcTextField
v-for="lang in langs"
v-model="newCustomCategory[lang]"
:label="lang"
/>
</template>
<template v-if="editCustomCategoryKey !== null">
<NcTextField
v-for="lang in langs"
v-model="categoriesCustom[editCustomCategoryKey][lang]"
:label="lang"
/>
</template>
</div>
</div>
<div v-if="section === 'apps'">
<table width="100%">
<tbody>
<tr v-for="item in apps" :key="key">
<td>
<img
:src="item.icon"
:alt="item.name"
/>
{{ item.name }}
</td>
<td width="50%">
<FormSelect
v-model="appsCategoriesCustom[item.id]"
:options="getOptions(categoriesCustom)"
:required="false"
/>
</td>
</tr>
</tbody>
</table>
</div>
<div class="modal__footer">
<template v-if="section === 'cats'">
<template v-if="newCustomCategory">
<NcActions>
<NcActionButton
icon="icon-close"
@click="cancelCustomCategory"
></NcActionButton>
</NcActions>
<NcActions>
<NcActionButton
icon="icon-checkmark"
@click="saveCustomCategory"
></NcActionButton>
</NcActions>
</template>
<template v-if="editCustomCategoryKey !== null">
<NcActions>
<NcActionButton
icon="icon-close"
@click="cancelCustomCategory"
></NcActionButton>
</NcActions>
</template>
<template v-else>
<NcActions>
<NcActionButton
v-if="!newCustomCategory && editCustomCategoryKey === null"
@click="addCustomCategory"
icon="icon-add"
></NcActionButton>
<NcActionButton
v-if="editCustomCategoryKey !== null"
@click="saveCustomCategory"
icon="icon-checkmark"
></NcActionButton>
</NcActions>
</template>
</template>
<NcButton
variant="primary"
@click="closeModal"
class="btn-close"
>
{{ t('side_menu', 'Close') }}
</NcButton>
</div>
</div>
</NcModal>
</template>
<script setup>
import { NcButton, NcModal, NcActions, NcActionButton, NcTextField } from '@nextcloud/vue'
import { useNavStore } from '../../../store/nav.js'
import { ref, onMounted, watch } from 'vue'
import FormSelect from './FormSelect'
const emit = defineEmits(['update:categoriesCustom', 'update:appsCategoriesCustom'])
const { categoriesCustom, appsCategoriesCustom, langs } = defineProps({
categoriesCustom: {
type: Array,
required: true,
},
appsCategoriesCustom: {
type: [Object, Array],
required: true,
},
langs: {
type: Array,
required: true,
},
})
const navStore = useNavStore()
const modal = ref(false)
const apps = ref([])
const categories = ref([])
const section = ref('apps')
const newCustomCategory = ref(null)
const editCustomCategoryKey = ref(null)
const appsCategoriesCustomNext = ref({})
const openModal = () => {
modal.value = true
}
const closeModal = () => {
modal.value = false
}
const setSection = (value) => {
section.value = value
}
const addCustomCategory = () => {
let data = {
id: 'cat' + Math.random().toString().replace('0.', ''),
}
langs.forEach((lang) => {
data[lang] = ''
})
newCustomCategory.value = data
}
const cancelCustomCategory = () => {
newCustomCategory.value = null
editCustomCategoryKey.value = null
}
const saveCustomCategory = () => {
const data = categoriesCustom
if (editCustomCategoryKey.value === null) {
data.push({...newCustomCategory.value})
}
emit('update:categoriesCustom', data)
newCustomCategory.value = null
editCustomCategoryKey.value = null
}
const removeCustomCategory = (key) => {
const data = categoriesCustom
delete data[key]
emit('update:categoriesCustom', Object.values(data))
}
const editCustomCategory = (key) => {
editCustomCategoryKey.value = key
}
const getOptions = (custom) => {
const data = []
custom.forEach((item) => {
data.push({id: item.id, label: item[langs[0]]})
})
categories.value.forEach((item) => {
data.push({id: item.categoryId, label: item.name !== '' ? item.name : t('side_menu', 'Other')})
})
data.sort((a, b) => (a.label < b.label ? -1 : 1))
return data
}
watch(() => appsCategoriesCustomNext, (value) => {
console.log(value)
})
onMounted(async () => {
apps.value = await navStore.getApps()
categories.value = await navStore.getCategories()
let value = {}
apps.value.forEach((app) => {
if (!appsCategoriesCustom[app.id]) {
value[app.id] = null
} else {
value[app.id] = appsCategoriesCustom[app.id]
}
})
emit('update:appsCategoriesCustom', value)
})
</script>
<style scoped>
.modal__content {
padding: 20px;
}
.menu button {
display: inline-block;
margin-right: 5px;
}
.modal__footer {
margin-top: 20px;
text-align: right;
}
.modal__footer button {
display: inline-block;
}
td {
padding: 5px 0;
}
tr:hover, td:hover {
background: none !important;
}
.form {
padding: 10px 0;
}
img {
width: 15px;
height: 15px;
}
.btn-close {
margin-left: 20px;
}
</style>

View file

@ -22,6 +22,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
v-model="model"
:multiple="multiple"
>
<option :value="null" v-if="!required"></option>
<option
v-for="option in options"
:key="option.id"
@ -49,12 +50,17 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<script setup>
import { NcCheckboxRadioSwitch } from '@nextcloud/vue'
const model = defineModel({ type: [Number, String, Array] })
const model = defineModel({ type: [Number, String, Array, null] })
const { options, expanded } = defineProps({
options: {
type: Array,
required: true,
},
required: {
type: Boolean,
required: false,
default: true,
},
expanded: {
type: Boolean,
required: false,

View file

@ -196,6 +196,24 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</TableValue>
</TableRow>
<TableRow>
<TableLabel
label="Customize application categories"
:top="true"
>
</TableLabel>
<TableValue>
<FormAppCategory
:langs="config['langs']"
:categories-custom="config['categories-custom']"
:apps-categories-custom="config['apps-categories-custom']"
@update:categories-custom="(value) => (config['categories-custom'] = value)"
@update:apps-categories-custom="(value) => (config['apps-categories-custom'] = value)"
/>
</TableValue>
</TableRow>
<TableRow>
<TableLabel
label="Customize sorting"
@ -565,6 +583,7 @@ import FormAppPicker from '../components/settings/form/FormAppPicker'
import FormAppSort from '../components/settings/form/FormAppSort'
import FormCatSort from '../components/settings/form/FormCatSort'
import FormDisplayPicker from '../components/settings/form/FormDisplayPicker'
import FormAppCategory from '../components/settings/form/FormAppCategory'
const menu = [
{ label: 'Global', section: 'global' },