feat(dashboard): enhance widget availability and initialization
All checks were successful
Deploy to production / deploy (push) Successful in 3m2s
All checks were successful
Deploy to production / deploy (push) Successful in 3m2s
- Updated `getAvailableWidgets` method in `DashboardService` to merge default widget types with database entries, ensuring immediate visibility of new widgets post-deployment. - Introduced `DASHBOARD_WIDGET_TYPE_DEFAULTS` in `initializeWidgetTypes` for canonical widget types, facilitating API merging when database entries are absent. - Modified `StatusBar.vue` to utilize a computed property for quick access children, improving menu item handling and visibility based on user context. - Enhanced `LoggedInView.vue` to dynamically return localized widget labels, improving user experience with accurate translations.
This commit is contained in:
@@ -1,24 +1,62 @@
|
|||||||
import BaseService from './BaseService.js';
|
import BaseService from './BaseService.js';
|
||||||
import UserDashboard from '../models/community/user_dashboard.js';
|
import UserDashboard from '../models/community/user_dashboard.js';
|
||||||
import WidgetType from '../models/type/widget_type.js';
|
import WidgetType from '../models/type/widget_type.js';
|
||||||
|
import { DASHBOARD_WIDGET_TYPE_DEFAULTS } from '../utils/initializeWidgetTypes.js';
|
||||||
|
|
||||||
class DashboardService extends BaseService {
|
class DashboardService extends BaseService {
|
||||||
/**
|
/**
|
||||||
* Liste aller möglichen (verfügbaren) Widget-Typen.
|
* Liste aller möglichen (verfügbaren) Widget-Typen.
|
||||||
* @returns {Promise<Array<{ id: number, label: string, endpoint: string, description: string|null, orderId: number }>>}
|
* Merge: Code-Defaults (immer) + DB-Zeilen; fehlende Defaults erscheinen mit synthetischer id,
|
||||||
|
* damit neue Widgets nach Deploy sofort unter „Widget hinzufügen“ sichtbar sind.
|
||||||
|
* @returns {Promise<Array<{ id: number|string, label: string, endpoint: string, description: string|null, orderId: number }>>}
|
||||||
*/
|
*/
|
||||||
async getAvailableWidgets() {
|
async getAvailableWidgets() {
|
||||||
const rows = await WidgetType.findAll({
|
const rows = await WidgetType.findAll({
|
||||||
order: [['orderId', 'ASC'], ['id', 'ASC']],
|
order: [['orderId', 'ASC'], ['id', 'ASC']],
|
||||||
attributes: ['id', 'label', 'endpoint', 'description', 'orderId']
|
attributes: ['id', 'label', 'endpoint', 'description', 'orderId']
|
||||||
});
|
});
|
||||||
return rows.map(r => ({
|
const dbByEndpoint = new Map(rows.map((r) => [r.endpoint, r]));
|
||||||
id: r.id,
|
const defaultEndpoints = new Set(DASHBOARD_WIDGET_TYPE_DEFAULTS.map((d) => d.endpoint));
|
||||||
label: r.label,
|
|
||||||
endpoint: r.endpoint,
|
const sortedDefaults = [...DASHBOARD_WIDGET_TYPE_DEFAULTS].sort(
|
||||||
description: r.description ?? null,
|
(a, b) => (a.orderId || 0) - (b.orderId || 0)
|
||||||
orderId: r.orderId
|
);
|
||||||
}));
|
|
||||||
|
const ordered = [];
|
||||||
|
for (const def of sortedDefaults) {
|
||||||
|
const existing = dbByEndpoint.get(def.endpoint);
|
||||||
|
if (existing) {
|
||||||
|
ordered.push({
|
||||||
|
id: existing.id,
|
||||||
|
label: existing.label,
|
||||||
|
endpoint: existing.endpoint,
|
||||||
|
description: existing.description ?? null,
|
||||||
|
orderId: existing.orderId
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
ordered.push({
|
||||||
|
id: `ep:${def.endpoint}`,
|
||||||
|
label: def.label,
|
||||||
|
endpoint: def.endpoint,
|
||||||
|
description: def.description ?? null,
|
||||||
|
orderId: def.orderId ?? 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const r of rows) {
|
||||||
|
if (!defaultEndpoints.has(r.endpoint)) {
|
||||||
|
ordered.push({
|
||||||
|
id: r.id,
|
||||||
|
label: r.label,
|
||||||
|
endpoint: r.endpoint,
|
||||||
|
description: r.description ?? null,
|
||||||
|
orderId: r.orderId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ordered;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
22
backend/sql/seed_dashboard_widget_types.sql
Normal file
22
backend/sql/seed_dashboard_widget_types.sql
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
-- Dashboard-Widget-Typen (type.widget_type)
|
||||||
|
-- Für Produktion ohne initializeWidgetTypes / ohne Deploy-Sync.
|
||||||
|
-- Idempotent: pro endpoint nur einfügen, wenn noch keine Zeile existiert.
|
||||||
|
--
|
||||||
|
-- Muss mit backend/utils/initializeWidgetTypes.js (DASHBOARD_WIDGET_TYPE_DEFAULTS)
|
||||||
|
-- übereinstimmen – bei neuen Widgets beides anpassen.
|
||||||
|
|
||||||
|
INSERT INTO type.widget_type (label, endpoint, description, order_id)
|
||||||
|
SELECT v.label, v.endpoint, v.description, v.order_id
|
||||||
|
FROM (
|
||||||
|
VALUES
|
||||||
|
('Termine'::text, '/api/termine'::text, 'Bevorstehende Termine'::text, 1),
|
||||||
|
('Falukant', '/api/falukant/dashboard-widget', 'Charakter, Geld, Nachrichten, Kinder', 2),
|
||||||
|
('News', '/api/news?language=de&category=top', 'Nachrichten (newsdata.io), Counter für Pagination', 3),
|
||||||
|
('Geburtstage', '/api/calendar/widget/birthdays', 'Nächste Geburtstage von Freunden', 4),
|
||||||
|
('Nächste Termine', '/api/calendar/widget/upcoming', 'Anstehende Kalendertermine', 5),
|
||||||
|
('Kalender', '/api/calendar/widget/mini', 'Mini-Kalenderansicht', 6),
|
||||||
|
('Sprachkurse', '/api/vocab/dashboard-widget', 'Vokabelkurse, aktuelle Lektion, Sprung zur Lektion', 7)
|
||||||
|
) AS v(label, endpoint, description, order_id)
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM type.widget_type wt WHERE wt.endpoint = v.endpoint
|
||||||
|
);
|
||||||
@@ -4,7 +4,8 @@ import WidgetType from '../models/type/widget_type.js';
|
|||||||
* Default rows for type.widget_type. Labels/descriptions are German for DB/admin readability;
|
* Default rows for type.widget_type. Labels/descriptions are German for DB/admin readability;
|
||||||
* the web app maps known `endpoint` values to localized strings (see LoggedInView / home.dashboard.widgetLabels).
|
* the web app maps known `endpoint` values to localized strings (see LoggedInView / home.dashboard.widgetLabels).
|
||||||
*/
|
*/
|
||||||
const DEFAULT_WIDGET_TYPES = [
|
/** Kanonische Widget-Typen (auch für API-Merge, falls DB noch keine Zeile hat) */
|
||||||
|
export const DASHBOARD_WIDGET_TYPE_DEFAULTS = [
|
||||||
{ label: 'Termine', endpoint: '/api/termine', description: 'Bevorstehende Termine', orderId: 1 },
|
{ label: 'Termine', endpoint: '/api/termine', description: 'Bevorstehende Termine', orderId: 1 },
|
||||||
{ label: 'Falukant', endpoint: '/api/falukant/dashboard-widget', description: 'Charakter, Geld, Nachrichten, Kinder', orderId: 2 },
|
{ label: 'Falukant', endpoint: '/api/falukant/dashboard-widget', description: 'Charakter, Geld, Nachrichten, Kinder', orderId: 2 },
|
||||||
{ label: 'News', endpoint: '/api/news?language=de&category=top', description: 'Nachrichten (newsdata.io), Counter für Pagination', orderId: 3 },
|
{ label: 'News', endpoint: '/api/news?language=de&category=top', description: 'Nachrichten (newsdata.io), Counter für Pagination', orderId: 3 },
|
||||||
@@ -17,9 +18,12 @@ const DEFAULT_WIDGET_TYPES = [
|
|||||||
/**
|
/**
|
||||||
* Stellt die Standard-Widget-Typen in type.widget_type bereit.
|
* Stellt die Standard-Widget-Typen in type.widget_type bereit.
|
||||||
* Idempotent: vorhandene Einträge werden nicht verändert.
|
* Idempotent: vorhandene Einträge werden nicht verändert.
|
||||||
|
*
|
||||||
|
* Produktion ohne Sync/Init: manuell backend/sql/seed_dashboard_widget_types.sql ausführen
|
||||||
|
* (muss dieselben Endpoints wie DASHBOARD_WIDGET_TYPE_DEFAULTS enthalten).
|
||||||
*/
|
*/
|
||||||
const initializeWidgetTypes = async () => {
|
const initializeWidgetTypes = async () => {
|
||||||
for (const row of DEFAULT_WIDGET_TYPES) {
|
for (const row of DASHBOARD_WIDGET_TYPE_DEFAULTS) {
|
||||||
await WidgetType.findOrCreate({
|
await WidgetType.findOrCreate({
|
||||||
where: { endpoint: row.endpoint },
|
where: { endpoint: row.endpoint },
|
||||||
defaults: {
|
defaults: {
|
||||||
|
|||||||
@@ -38,11 +38,11 @@
|
|||||||
class="statusbar-section statusbar-section--nav"
|
class="statusbar-section statusbar-section--nav"
|
||||||
>
|
>
|
||||||
<div class="quick-access">
|
<div class="quick-access">
|
||||||
<template v-for="(menuItem, key) in menu.falukant.children" :key="menuItem.id" >
|
<template v-for="[key, menuItem] in falukantQuickAccessChildren" :key="menuItem.id || key" >
|
||||||
<img
|
<img
|
||||||
:src="'/images/icons/falukant/shortmap/' + key + '.png'"
|
:src="'/images/icons/falukant/shortmap/' + key + '.png'"
|
||||||
class="menu-icon"
|
class="menu-icon"
|
||||||
@click="openPage(menuItem)"
|
@click="openPage(menuItem.path)"
|
||||||
:title="$t(`navigation.m-falukant.${key}`)"
|
:title="$t(`navigation.m-falukant.${key}`)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
@@ -96,6 +96,16 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
...mapState(["socket", "daemonSocket", "user"]),
|
...mapState(["socket", "daemonSocket", "user"]),
|
||||||
...mapGetters(['menu']),
|
...mapGetters(['menu']),
|
||||||
|
/** Menü kann hinter /info zurückbleiben: „Erstellen“ ausblenden, sobald ein Account erkennbar ist. */
|
||||||
|
falukantQuickAccessChildren() {
|
||||||
|
const ch = this.menu?.falukant?.children;
|
||||||
|
if (!ch || typeof ch !== 'object') return [];
|
||||||
|
const keys = Object.keys(ch);
|
||||||
|
const hasOtherThanCreate = keys.some((k) => k !== 'create');
|
||||||
|
const hasCharacterName = Boolean((this.characterName || '').trim());
|
||||||
|
const hideCreate = hasOtherThanCreate || hasCharacterName;
|
||||||
|
return Object.entries(ch).filter(([key]) => !(key === 'create' && hideCreate));
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
// Wenn sich das Menü ändert, lade die Bilder neu
|
// Wenn sich das Menü ändert, lade die Bilder neu
|
||||||
@@ -140,12 +150,10 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
preloadQuickAccessImages() {
|
preloadQuickAccessImages() {
|
||||||
// Lade alle Schnellzugriffs-Bilder vor, damit sie gecacht werden
|
// Lade alle Schnellzugriffs-Bilder vor, damit sie gecacht werden
|
||||||
if (this.menu.falukant && this.menu.falukant.children) {
|
this.falukantQuickAccessChildren.forEach(([key]) => {
|
||||||
Object.keys(this.menu.falukant.children).forEach(key => {
|
const img = new Image();
|
||||||
const img = new Image();
|
img.src = `/images/icons/falukant/shortmap/${key}.png`;
|
||||||
img.src = `/images/icons/falukant/shortmap/${key}.png`;
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lade auch andere häufig verwendete Bilder vor
|
// Lade auch andere häufig verwendete Bilder vor
|
||||||
const commonImages = [
|
const commonImages = [
|
||||||
|
|||||||
@@ -186,15 +186,18 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
getLocalizedWidgetLabel(endpoint, fallbackLabel = '') {
|
getLocalizedWidgetLabel(endpoint, fallbackLabel = '') {
|
||||||
|
const ep = String(endpoint || '');
|
||||||
|
if (ep.startsWith('/api/news')) {
|
||||||
|
return this.$t('home.dashboard.widgetLabels.news');
|
||||||
|
}
|
||||||
const key = {
|
const key = {
|
||||||
'/api/termine': 'home.dashboard.widgetLabels.appointments',
|
'/api/termine': 'home.dashboard.widgetLabels.appointments',
|
||||||
'/api/falukant/dashboard-widget': 'home.dashboard.widgetLabels.falukant',
|
'/api/falukant/dashboard-widget': 'home.dashboard.widgetLabels.falukant',
|
||||||
'/api/news': 'home.dashboard.widgetLabels.news',
|
|
||||||
'/api/calendar/widget/birthdays': 'home.dashboard.widgetLabels.birthdays',
|
'/api/calendar/widget/birthdays': 'home.dashboard.widgetLabels.birthdays',
|
||||||
'/api/calendar/widget/upcoming': 'home.dashboard.widgetLabels.upcoming',
|
'/api/calendar/widget/upcoming': 'home.dashboard.widgetLabels.upcoming',
|
||||||
'/api/calendar/widget/mini': 'home.dashboard.widgetLabels.calendar',
|
'/api/calendar/widget/mini': 'home.dashboard.widgetLabels.calendar',
|
||||||
'/api/vocab/dashboard-widget': 'home.dashboard.widgetLabels.vocabCourses'
|
'/api/vocab/dashboard-widget': 'home.dashboard.widgetLabels.vocabCourses'
|
||||||
}[endpoint];
|
}[ep];
|
||||||
return key ? this.$t(key) : fallbackLabel;
|
return key ? this.$t(key) : fallbackLabel;
|
||||||
},
|
},
|
||||||
normalizeWidgetType(widgetType) {
|
normalizeWidgetType(widgetType) {
|
||||||
|
|||||||
Reference in New Issue
Block a user