From 4762818d3d93a3ef3bc49fb2ce1aba7d2dcdad27 Mon Sep 17 00:00:00 2001 From: ValueOn AG Date: Sun, 12 Apr 2026 14:05:01 +0200 Subject: [PATCH] ui fixes --- src/api/subscriptionApi.ts | 13 +- .../FormGeneratorForm/FormGeneratorForm.tsx | 18 +- .../FormGeneratorTable/FormGeneratorTable.tsx | 100 +---- .../Navigation/MandateNavigation.tsx | 71 ++-- src/components/UnifiedDataBar/FilesTab.tsx | 11 +- src/contexts/FileContext.tsx | 20 + src/hooks/useAdminRbacRoles.ts | 23 +- src/hooks/useMandateRoles.ts | 6 +- src/hooks/useSubscription.ts | 7 + src/pages/Settings.tsx | 45 +-- src/pages/admin/AdminFeatureRolesPage.tsx | 6 +- src/pages/admin/AdminMandateRolesPage.tsx | 13 +- src/pages/basedata/FilesPage.tsx | 17 +- src/pages/basedata/PromptsPage.tsx | 12 +- src/pages/billing/AdminSubscriptionsPage.tsx | 4 +- src/pages/billing/BillingDataView.tsx | 82 ++-- src/pages/billing/SubscriptionTab.tsx | 362 +++++++++++------- src/utils/attributeTypeMapper.ts | 2 +- 18 files changed, 434 insertions(+), 378 deletions(-) diff --git a/src/api/subscriptionApi.ts b/src/api/subscriptionApi.ts index 6096257..70379c3 100644 --- a/src/api/subscriptionApi.ts +++ b/src/api/subscriptionApi.ts @@ -10,8 +10,8 @@ export type BillingPeriod = 'MONTHLY' | 'YEARLY' | 'NONE'; export interface SubscriptionPlan { planKey: string; selectableByUser: boolean; - title: Record; - description: Record; + title: string; + description: string; currency: string; billingPeriod: BillingPeriod; pricePerUserCHF: number; @@ -44,11 +44,20 @@ export interface MandateSubscription { stripeSubscriptionId: string | null; } +export interface SubscriptionUsage { + activeUsers: number; + activeInstances: number; + usedStorageMB: number; + maxStorageMB: number | null; + storagePercent: number | null; +} + export interface SubscriptionStatusResponse { active: boolean; subscription: MandateSubscription | null; plan: SubscriptionPlan | null; scheduled: MandateSubscription | null; + usage: SubscriptionUsage | null; } export interface ActivatePlanResponse { diff --git a/src/components/FormGenerator/FormGeneratorForm/FormGeneratorForm.tsx b/src/components/FormGenerator/FormGeneratorForm/FormGeneratorForm.tsx index 98b1ea0..3cf43e7 100644 --- a/src/components/FormGenerator/FormGeneratorForm/FormGeneratorForm.tsx +++ b/src/components/FormGenerator/FormGeneratorForm/FormGeneratorForm.tsx @@ -654,26 +654,28 @@ export function FormGeneratorForm>({ ]; for (const lang of availableLanguages) { if (lang.code === 'xx') continue; - langs.push({ code: lang.code, uiLabel: lang.code.toUpperCase(), required: false }); + langs.push({ code: lang.code, uiLabel: lang.label || lang.code.toUpperCase(), required: false }); } return langs; }, [availableLanguages, t]); const _handleAutoTranslate = async (attrName: string, multilingualValue: Record) => { - const sourceLang = multilingualLangs.find(l => (multilingualValue[l.code] || '').trim())?.code; - if (!sourceLang) return; - const sourceText = (multilingualValue[sourceLang] || '').trim(); + const sourceLangEntry = multilingualLangs.find(l => (multilingualValue[l.code] || '').trim()); + if (!sourceLangEntry) return; + const sourceText = (multilingualValue[sourceLangEntry.code] || '').trim(); if (!sourceText) return; - const targetLangs = multilingualLangs.map(l => l.code).filter(c => c !== sourceLang); - if (!targetLangs.length) return; + const targets = multilingualLangs + .filter(l => l.code !== sourceLangEntry.code && l.code !== 'xx') + .map(l => ({ code: l.code, label: l.uiLabel })); + if (!targets.length) return; setTranslatingField(attrName); try { const res = await api.post('/api/i18n/translate-field', { sourceText, - sourceLang, - targetLangs, + sourceLang: sourceLangEntry.code, + targetLangs: targets, }); const translations: Record = res.data?.translations || {}; const newValue = { ...multilingualValue }; diff --git a/src/components/FormGenerator/FormGeneratorTable/FormGeneratorTable.tsx b/src/components/FormGenerator/FormGeneratorTable/FormGeneratorTable.tsx index eaa5df0..dd8987d 100644 --- a/src/components/FormGenerator/FormGeneratorTable/FormGeneratorTable.tsx +++ b/src/components/FormGenerator/FormGeneratorTable/FormGeneratorTable.tsx @@ -78,33 +78,21 @@ import api from '../../../api'; // FK Cache type: maps fkSource -> { id -> displayLabel } type FkCacheType = Record>; -const isTextMultilingual = (value: any): boolean => { - if (!value || typeof value !== 'object' || Array.isArray(value) || value instanceof Date) { - return false; +/** + * Stringify any cell value for display. + * The backend resolves TextMultilingual to plain strings via resolveText() / get_text(). + * If an unresolved object still arrives, extract xx as a safe fallback and log a warning. + */ +const _objectToDisplayString = (value: Record): string => { + if ('xx' in value && typeof value.xx === 'string' && value.xx.trim()) { + console.warn('FormGeneratorTable: received unresolved TextMultilingual object — backend should call resolveText()', value); + return value.xx as string; } - return 'en' in value && typeof value.en === 'string'; -}; - -const formatTextMultilingual = (value: any, currentLanguage?: string): string => { - if (!isTextMultilingual(value)) { - return String(value); + for (const field of ['label', 'name', 'title', 'id', 'value', 'text']) { + const v = value[field]; + if (v !== undefined && v !== null) return String(v); } - - if (currentLanguage && value[currentLanguage] && typeof value[currentLanguage] === 'string' && value[currentLanguage].trim()) { - return value[currentLanguage]; - } - - if (value.en && typeof value.en === 'string' && value.en.trim()) { - return value.en; - } - - for (const key of Object.keys(value)) { - if (key !== 'en' && value[key] && typeof value[key] === 'string' && value[key].trim()) { - return value[key]; - } - } - - return '-'; + try { return JSON.stringify(value); } catch { return String(value); } }; // Types for the FormGeneratorTable @@ -575,9 +563,7 @@ export function FormGeneratorTable>({ } }).current; - // Helper function to convert any field value to display string - // Handles: string, boolean, number, TextMultilingual, objects - const convertToDisplayString = useCallback((fieldValue: any, language: string): string => { + const convertToDisplayString = useCallback((fieldValue: any, _language: string): string => { if (fieldValue === null || fieldValue === undefined) { return '-'; } @@ -597,18 +583,8 @@ export function FormGeneratorTable>({ return fieldValue; } - // Object - check for TextMultilingual (has 'en' key) if (typeof fieldValue === 'object' && fieldValue !== null) { - if ('en' in fieldValue) { - return formatTextMultilingual(fieldValue, language); - } - - // Other objects → try to stringify - try { - return JSON.stringify(fieldValue); - } catch { - return String(fieldValue); - } + return _objectToDisplayString(fieldValue as Record); } // Fallback @@ -1299,11 +1275,7 @@ export function FormGeneratorTable>({ if (typeof val === 'boolean') { str = val ? 'true' : 'false'; } else if (typeof val === 'object') { - if (isTextMultilingual(val)) { - str = formatTextMultilingual(val, currentLanguage); - } else { - try { str = JSON.stringify(val); } catch { str = String(val); } - } + str = _objectToDisplayString(val as Record); } else { str = String(val); } @@ -1554,45 +1526,9 @@ export function FormGeneratorTable>({ } } - // Handle objects/arrays (e.g., references to other entities) - // Check if value is an object (but not Date, Array, or null) + // Handle objects (e.g., references to other entities, or unresolved TextMultilingual) if (typeof value === 'object' && value !== null && !(value instanceof Date) && !Array.isArray(value)) { - // Check if this is a TextMultilingual object first - if (isTextMultilingual(value)) { - return formatTextMultilingual(value, currentLanguage); - } - - // Try to find a display field in common order: label, name, title, id - const displayFields = ['label', 'name', 'title', 'id', 'value', 'text']; - for (const field of displayFields) { - if (value[field] !== undefined && value[field] !== null) { - const displayValue = value[field]; - // If the display value is itself an object, check if it's TextMultilingual - if (typeof displayValue === 'object' && displayValue !== null && !Array.isArray(displayValue) && !(displayValue instanceof Date)) { - if (isTextMultilingual(displayValue)) { - return formatTextMultilingual(displayValue, currentLanguage); - } - try { - return JSON.stringify(displayValue); - } catch { - return String(displayValue); - } - } - return String(displayValue); - } - } - // If no display field found, try to stringify the object (limited to avoid huge output) - try { - const stringified = JSON.stringify(value); - // Truncate if too long - if (stringified.length > 100) { - return stringified.substring(0, 97) + '...'; - } - return stringified; - } catch { - // If stringification fails, show object type - return `[${value.constructor?.name || 'Object'}]`; - } + return _objectToDisplayString(value as Record); } // Handle arrays (e.g., multiselect values) diff --git a/src/components/Navigation/MandateNavigation.tsx b/src/components/Navigation/MandateNavigation.tsx index b7ae1b0..c105852 100644 --- a/src/components/Navigation/MandateNavigation.tsx +++ b/src/components/Navigation/MandateNavigation.tsx @@ -47,12 +47,12 @@ type NavTranslateFn = (key: string, params?: Record) => /** * Convert a NavigationItem (from static block) to TreeNodeItem. - * Labels from the backend are German i18n keys — translate via t(). + * Labels are already translated by the backend via t(). */ -function navigationItemToTreeNode(item: NavigationItem, tr: NavTranslateFn): TreeNodeItem { +function _navigationItemToTreeNode(item: NavigationItem): TreeNodeItem { return { id: item.objectKey, - label: tr(item.uiLabel), + label: item.uiLabel, icon: getPageIcon(item.uiComponent), path: item.uiPath, }; @@ -66,25 +66,24 @@ function _staticItemsToTreeNode( id: string, label: string, items: NavigationItem[], - tr: NavTranslateFn, defaultExpanded: boolean = true, ): TreeNodeItem { return { id, label, - children: items.map(i => navigationItemToTreeNode(i, tr)), + children: items.map(i => _navigationItemToTreeNode(i)), defaultExpanded, }; } /** * Convert a FeatureView to TreeNodeItem. - * View labels are German i18n keys — translate via t(). + * View labels are already translated by the backend. */ -function featureViewToTreeNode(view: FeatureView, tr: NavTranslateFn): TreeNodeItem { +function _featureViewToTreeNode(view: FeatureView): TreeNodeItem { return { id: view.objectKey, - label: tr(view.uiLabel), + label: view.uiLabel, path: view.uiPath, }; } @@ -95,17 +94,17 @@ function featureViewToTreeNode(view: FeatureView, tr: NavTranslateFn): TreeNodeI * Shows the feature icon next to the instance name for visual distinction. * If user is instance admin, a rename icon appears on hover. */ -function featureInstanceToTreeNode( +function _featureInstanceToTreeNode( instance: FeatureInstance, featureUiComponent: string, onRename: ((instanceId: string, currentLabel: string) => void) | undefined, - tr: NavTranslateFn, + t: NavTranslateFn, ): TreeNodeItem { - const children = instance.views.map(v => featureViewToTreeNode(v, tr)); + const children = instance.views.map(v => _featureViewToTreeNode(v)); const renameAction = instance.isAdmin && onRename ? (