Merge pull request #3615 from modoboa/feature/theme_simple_customization

Simple and stupid way to customize theme colors.
This commit is contained in:
Antoine Nguyen
2025-07-26 19:19:49 +02:00
committed by GitHub
10 changed files with 206 additions and 5 deletions
+41 -1
View File
@@ -4,4 +4,44 @@
</Suspense>
</template>
<script setup></script>
<script setup>
import { useTheme } from 'vuetify'
import themeApi from '@/api/theme'
const theme = useTheme()
themeApi.getTheme().then((response) => {
if (
response.data.theme_primary_color !==
theme.themes.value.light.colors.primary
) {
theme.themes.value.light.colors.primary = response.data.theme_primary_color
}
if (
response.data.theme_primary_color_light !==
theme.themes.value.light.colors['primary-lighten-1']
) {
theme.themes.value.light.colors['primary-lighten-1'] =
response.data.theme_primary_color_light
}
if (
response.data.theme_primary_color_dark !==
theme.themes.value.light.colors['primary-darken-1']
) {
theme.themes.value.light.colors['primary-darken-1'] =
response.data.theme_primary_color_dark
}
if (
response.data.theme_secondary_color !==
theme.themes.value.light.colors.secondary
) {
theme.themes.value.light.colors.secondary =
response.data.theme_secondary_color
}
if (
response.data.theme_label_color !== theme.themes.value.light.colors.label
) {
theme.themes.value.light.colors.label = response.data.theme_label_color
}
})
</script>
+7
View File
@@ -0,0 +1,7 @@
import repository from './repository'
export default {
getTheme() {
return repository.get(`/theme/`)
},
}
@@ -68,6 +68,16 @@
v-model="parameters[param.name]"
>
</HtmlEditor>
<v-color-picker
v-else-if="param.widget === 'ColorPicker'"
v-model="parameters[param.name]"
:hint="param.help_text"
persistent-hint
:error="formErrors[param.name] !== undefined"
:error-messages="formErrors[param.name]"
density="compact"
variant="outlined"
/>
<v-text-field
v-else
v-model="parameters[param.name]"
+15 -1
View File
@@ -1,6 +1,6 @@
<template>
<v-toolbar flat>
<v-toolbar-title>{{ $gettext('Welcome to Modoboa') }}</v-toolbar-title>
<v-toolbar-title>{{ welcomeMsg }}</v-toolbar-title>
</v-toolbar>
<v-row>
@@ -11,7 +11,21 @@
</template>
<script setup>
import { ref } from 'vue'
import { useGettext } from 'vue3-gettext'
import NewsFeedWidget from '@/components/admin/dashboard/NewsFeedWidget'
import parametersApi from '@/api/parameters'
const { $gettext } = useGettext()
const welcomeMsg = ref('')
const response = await parametersApi.getGlobalApplication('core')
if (response.data.params.custom_welcome_message) {
welcomeMsg.value = response.data.params.custom_welcome_message
} else {
welcomeMsg.value = $gettext('Welcome to Modoboa')
}
</script>
<style scoped>
+17
View File
@@ -141,11 +141,19 @@ class CoreGlobalParametersSerializer(serializers.Serializer):
)
# Dashboard settings
custom_welcome_message = serializers.CharField(required=False)
rss_feed_url = serializers.URLField(
allow_blank=True, required=False, allow_null=True
)
hide_features_widget = serializers.BooleanField(default=False)
# Theme settings
theme_primary_color = serializers.CharField(default="#046BF8")
theme_primary_color_dark = serializers.CharField(default="#0350BA")
theme_primary_color_light = serializers.CharField(default="#3688F9")
theme_secondary_color = serializers.CharField(default="#F18429")
theme_label_color = serializers.CharField(default="#616161")
# Notification settings
sender_address = lib_fields.DRFEmailFieldUTF8(default="noreply@yourdomain.test")
@@ -654,3 +662,12 @@ class NewsFeedEntrySerializer(serializers.Serializer):
title = serializers.CharField()
link = serializers.CharField()
published = serializers.DateTimeField()
class ThemeSerializer(serializers.Serializer):
theme_primary_color = serializers.CharField(default="#046BF8")
theme_primary_color_dark = serializers.CharField(default="#0350BA")
theme_primary_color_light = serializers.CharField(default="#3688F9")
theme_secondary_color = serializers.CharField(default="#F18429")
theme_label_color = serializers.CharField(default="#616161")
+16
View File
@@ -545,6 +545,22 @@ class CapabilitiesAPITestCase(ModoAPITestCase):
self.assertIn("rspamd", response.json()["capabilities"])
class ThemeAPITestCase(ModoAPITestCase):
def test_get(self):
url = reverse("v2:theme")
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
theme = response.json()
self.assertEqual(theme["theme_primary_color"], "#046BF8")
self.set_global_parameter("theme_primary_color", "#FF0000")
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
theme = response.json()
self.assertEqual(theme["theme_primary_color"], "#FF0000")
class NewsFeedAPIViewTestCase(ModoAPITestCase):
@classmethod
+1
View File
@@ -43,4 +43,5 @@ urlpatterns += [
name="news-feed",
),
path("capabilities/", views.CapabilitiesAPIView.as_view(), name="capabilities"),
path("theme/", views.ThemeAPIView.as_view(), name="theme"),
]
+21
View File
@@ -179,6 +179,27 @@ class CapabilitiesAPIView(APIView):
return response.Response({"capabilities": get_capabilities()})
class ThemeAPIView(APIView):
"""Return the theme values of this Modoboa instance."""
permission_classes = [permissions.IsAuthenticated]
throttle_classes = [UserLesserDdosUser]
@extend_schema(responses=serializers.ThemeSerializer)
def get(self, request, *args, **kwargs):
values = {}
params = [
"theme_primary_color",
"theme_primary_color_dark",
"theme_primary_color_light",
"theme_secondary_color",
"theme_label_color",
]
for param in params:
values[param] = request.localconfig.parameters.get_value(param)
return response.Response(values)
class NewsFeedAPIView(APIView):
"""Return list of latest news from configured RSS feed."""
+77 -2
View File
@@ -99,12 +99,14 @@ GLOBAL_PARAMETERS_STRUCT = collections.OrderedDict(
(
"allow_special_characters",
{
"label": gettext_lazy("Allow special characters for random password"),
"label": gettext_lazy(
"Allow special characters for random password"
),
"help_text": gettext_lazy(
"Enable special characters in randomly generated "
"passwords."
),
}
},
),
(
"update_password_url",
@@ -478,6 +480,15 @@ GLOBAL_PARAMETERS_STRUCT = collections.OrderedDict(
"label": gettext_lazy("Dashboard"),
"params": collections.OrderedDict(
[
(
"custom_welcome_message",
{
"label": gettext_lazy("Custom welcome message"),
"help_text": gettext_lazy(
"A welcome message that will be displayed instead the default one"
),
},
),
(
"rss_feed_url",
{
@@ -502,6 +513,70 @@ GLOBAL_PARAMETERS_STRUCT = collections.OrderedDict(
),
},
),
(
"theme",
{
"label": gettext_lazy("Theme"),
"params": collections.OrderedDict(
[
(
"theme_primary_color",
{
"label": gettext_lazy("Primary theme color"),
"help": gettext_lazy(
"Primary color of the theme (hexadecimal code), used as the background color of the left menu for example"
),
"widget": "ColorPicker",
},
),
(
"theme_primary_color_light",
{
"label": gettext_lazy(
"Primary theme color (lighter version)"
),
"help": gettext_lazy(
"Primary color of the theme (hexadecimal code)"
),
"widget": "ColorPicker",
},
),
(
"theme_primary_color_dark",
{
"label": gettext_lazy(
"Primary theme color (darker version)"
),
"help": gettext_lazy(
"Primary color of the theme (hexadecimal code)"
),
"widget": "ColorPicker",
},
),
(
"theme_secondary_color",
{
"label": gettext_lazy("Secondary theme color"),
"help": gettext_lazy(
"Secondary color of the theme (hexadecimal code), used as the background color of some buttons for example"
),
"widget": "ColorPicker",
},
),
(
"theme_label_color",
{
"label": gettext_lazy("Labels theme color"),
"help": gettext_lazy(
"Theme color for labels (hexadecimal code)"
),
"widget": "ColorPicker",
},
),
]
),
},
),
(
"notifications",
{
+1 -1
View File
@@ -134,7 +134,7 @@ def send_mail(request, attributes: dict, attachments: list) -> tuple[bool, str |
{
"backend": "modoboa.lib.smtp_backend.OAuthBearerEmailBackend",
"username": request.user.email,
"password": request.auth,
"password": str(request.auth),
}
)
else: