ux cleanup panel basics
Some checks failed
Deploy Nyla Frontend to Integration / deploy (push) Failing after 53s
Some checks failed
Deploy Nyla Frontend to Integration / deploy (push) Failing after 53s
This commit is contained in:
parent
d579df1c92
commit
766a767d59
26 changed files with 358 additions and 204 deletions
68
src/api.ts
68
src/api.ts
|
|
@ -106,18 +106,27 @@ api.interceptors.request.use(
|
|||
}
|
||||
);
|
||||
|
||||
// Add a response interceptor to handle token expiration
|
||||
// Silent refresh: attempt token renewal before forcing re-login
|
||||
let _isRefreshing = false;
|
||||
let _refreshSubscribers: Array<(success: boolean) => void> = [];
|
||||
|
||||
function _onRefreshDone(success: boolean) {
|
||||
_refreshSubscribers.forEach(cb => cb(success));
|
||||
_refreshSubscribers = [];
|
||||
}
|
||||
|
||||
api.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (error) => {
|
||||
if (error.response?.status === 401) {
|
||||
// Don't redirect to login if the request was to a login endpoint
|
||||
const isLoginEndpoint = error.config?.url?.includes('/login') ||
|
||||
error.config?.url?.includes('/api/local/login') ||
|
||||
error.config?.url?.includes('/api/msft/auth/login') ||
|
||||
error.config?.url?.includes('/api/google/auth/login');
|
||||
const originalRequest = error.config;
|
||||
|
||||
if (error.response?.status === 401) {
|
||||
const isAuthEndpoint = originalRequest?.url?.includes('/login') ||
|
||||
originalRequest?.url?.includes('/api/local/login') ||
|
||||
originalRequest?.url?.includes('/api/local/refresh') ||
|
||||
originalRequest?.url?.includes('/api/msft/auth/login') ||
|
||||
originalRequest?.url?.includes('/api/google/auth/login');
|
||||
|
||||
// Don't redirect if we're on a public auth page (prevents redirect loops and allows public pages to work)
|
||||
const pathname = window.location.pathname;
|
||||
const isOnPublicAuthPage = pathname === '/login' ||
|
||||
pathname.startsWith('/login') ||
|
||||
|
|
@ -129,19 +138,52 @@ api.interceptors.response.use(
|
|||
pathname.startsWith('/password-reset-request') ||
|
||||
pathname.startsWith('/invite');
|
||||
|
||||
if (!isLoginEndpoint && !isOnPublicAuthPage) {
|
||||
// Clear local auth data (httpOnly cookies are cleared by backend)
|
||||
if (isAuthEndpoint || isOnPublicAuthPage) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
// Attempt silent refresh (only once per request)
|
||||
if (!originalRequest._retryAfterRefresh) {
|
||||
originalRequest._retryAfterRefresh = true;
|
||||
|
||||
if (!_isRefreshing) {
|
||||
_isRefreshing = true;
|
||||
try {
|
||||
await api.post('/api/local/refresh');
|
||||
_isRefreshing = false;
|
||||
_onRefreshDone(true);
|
||||
return api(originalRequest);
|
||||
} catch {
|
||||
_isRefreshing = false;
|
||||
_onRefreshDone(false);
|
||||
sessionStorage.removeItem('auth_authority');
|
||||
clearUserDataCache();
|
||||
// Redirect to login
|
||||
window.location.href = '/login';
|
||||
return Promise.reject(error);
|
||||
}
|
||||
} else {
|
||||
// Another request is already refreshing; queue this one
|
||||
return new Promise((resolve, reject) => {
|
||||
_refreshSubscribers.push((success: boolean) => {
|
||||
if (success) {
|
||||
resolve(api(originalRequest));
|
||||
} else {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Handle rate limiting (429) - don't throw, just log and return error
|
||||
// Refresh already failed for this request
|
||||
sessionStorage.removeItem('auth_authority');
|
||||
clearUserDataCache();
|
||||
window.location.href = '/login';
|
||||
}
|
||||
|
||||
// Handle rate limiting (429)
|
||||
if (error.response?.status === 429) {
|
||||
console.warn('Rate limit exceeded (429). Please wait before making more requests.');
|
||||
// Don't cause cascading errors by throwing here
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
|
|
|
|||
|
|
@ -3332,7 +3332,9 @@ export function FormGeneratorTable<T extends Record<string, any>>({
|
|||
colSpan={(selectable ? 1 : 0) + (hasActionColumn ? 1 : 0) + detectedColumns.length}
|
||||
style={{ textAlign: 'center', padding: '40px 16px', color: 'var(--text-muted, #64748b)' }}
|
||||
>
|
||||
{emptyMessage || t('Keine Daten verfügbar')}
|
||||
{supportsBackendPagination && !hookDataProp?.pagination
|
||||
? t('Lade Daten...')
|
||||
: (emptyMessage || t('Keine Daten verfügbar'))}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
* Tab D: Neutralization Mappings — FormGeneratorTable + delete
|
||||
*/
|
||||
|
||||
import React, { useState, useCallback, useEffect, useMemo } from 'react';
|
||||
import React, { useState, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import {
|
||||
ResponsiveContainer, LineChart, Line, XAxis, YAxis, CartesianGrid,
|
||||
|
|
@ -195,6 +195,10 @@ export const ComplianceAuditPage: React.FC = () => {
|
|||
|
||||
const selectedEntryId = searchParams.get('entryId');
|
||||
|
||||
const _lastAiLogParams = useRef<any>(undefined);
|
||||
const _lastAuditLogParams = useRef<any>(undefined);
|
||||
const _lastNeutParams = useRef<any>(undefined);
|
||||
|
||||
// ── Mandate loader ──
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -222,6 +226,8 @@ export const ComplianceAuditPage: React.FC = () => {
|
|||
|
||||
const _loadAiLog = useCallback(async (paginationParams?: any) => {
|
||||
if (!selectedMandateId) return;
|
||||
if (paginationParams) _lastAiLogParams.current = paginationParams;
|
||||
else paginationParams = _lastAiLogParams.current;
|
||||
setAiLoading(true);
|
||||
try {
|
||||
const page = paginationParams?.page ?? 1;
|
||||
|
|
@ -254,6 +260,8 @@ export const ComplianceAuditPage: React.FC = () => {
|
|||
|
||||
const _loadAuditLog = useCallback(async (paginationParams?: any) => {
|
||||
if (!selectedMandateId) return;
|
||||
if (paginationParams) _lastAuditLogParams.current = paginationParams;
|
||||
else paginationParams = _lastAuditLogParams.current;
|
||||
setAuditLoading(true);
|
||||
try {
|
||||
const page = paginationParams?.page ?? 1;
|
||||
|
|
@ -301,6 +309,8 @@ export const ComplianceAuditPage: React.FC = () => {
|
|||
|
||||
const _loadNeutMappings = useCallback(async (paginationParams?: any) => {
|
||||
if (!selectedMandateId) return;
|
||||
if (paginationParams) _lastNeutParams.current = paginationParams;
|
||||
else paginationParams = _lastNeutParams.current;
|
||||
setNeutLoading(true);
|
||||
try {
|
||||
const page = paginationParams?.page ?? 1;
|
||||
|
|
@ -369,10 +379,7 @@ export const ComplianceAuditPage: React.FC = () => {
|
|||
|
||||
useEffect(() => {
|
||||
if (!selectedMandateId) return;
|
||||
if (activeTab === 'ai-log') void _loadAiLog();
|
||||
else if (activeTab === 'audit-log') void _loadAuditLog();
|
||||
else if (activeTab === 'stats') void _loadStats({ dateFrom: statsPeriod.fromDate, dateTo: statsPeriod.toDate });
|
||||
else if (activeTab === 'neutralization') void _loadNeutMappings();
|
||||
if (activeTab === 'stats') void _loadStats({ dateFrom: statsPeriod.fromDate, dateTo: statsPeriod.toDate });
|
||||
}, [activeTab, selectedMandateId]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
// ── Content view handler (modal) ──
|
||||
|
|
@ -866,6 +873,7 @@ export const ComplianceAuditPage: React.FC = () => {
|
|||
filterable={true}
|
||||
searchable={true}
|
||||
selectable={false}
|
||||
initialSort={[{ key: 'timestamp', direction: 'desc' }]}
|
||||
emptyMessage={t('Keine Audit-Einträge vorhanden.')}
|
||||
onRefresh={_loadAuditLog}
|
||||
hookData={auditLogHookData}
|
||||
|
|
@ -891,6 +899,7 @@ export const ComplianceAuditPage: React.FC = () => {
|
|||
filterable={true}
|
||||
searchable={true}
|
||||
selectable={false}
|
||||
initialSort={[{ key: 'timestamp', direction: 'desc' }]}
|
||||
emptyMessage={t('Keine AI-Audit-Einträge vorhanden.')}
|
||||
onRefresh={_loadAiLog}
|
||||
hookData={aiLogHookData}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,8 @@ const _downloadJson = (data: unknown, fileName: string, mimeType = 'application/
|
|||
|
||||
export const GDPRPage: React.FC = () => {
|
||||
const { t } = useLanguage();
|
||||
const contactEmail = 'p.motsch@poweron.swiss';
|
||||
const contactEmail = 'privacy@poweron.swiss';
|
||||
const deleteWord = t('LOESCHEN');
|
||||
const [consentInfo, setConsentInfo] = useState<ConsentInfo | null>(null);
|
||||
const [isLoadingConsent, setIsLoadingConsent] = useState(true);
|
||||
const [consentError, setConsentError] = useState<string | null>(null);
|
||||
|
|
@ -123,8 +124,8 @@ export const GDPRPage: React.FC = () => {
|
|||
|
||||
const handleDeleteAccount = async () => {
|
||||
setActionMessage(null);
|
||||
if (deleteConfirmText !== 'LOESCHEN') {
|
||||
setActionMessage({ type: 'error', text: t('Bitte geben Sie LOESCHEN ein, um die Löschung zu bestätigen.') });
|
||||
if (deleteConfirmText !== deleteWord) {
|
||||
setActionMessage({ type: 'error', text: t('Bitte geben Sie {word} ein, um die Löschung zu bestätigen.', { word: deleteWord }) });
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -227,14 +228,14 @@ export const GDPRPage: React.FC = () => {
|
|||
<div className={styles.deleteConfirm}>
|
||||
<p className={styles.deleteWarning}>
|
||||
{t('Diese Aktion ist unwiderruflich. Geben Sie {word} ein, um zu bestätigen.', {
|
||||
word: 'LOESCHEN',
|
||||
word: deleteWord,
|
||||
})}
|
||||
</p>
|
||||
<input
|
||||
className={styles.deleteInput}
|
||||
value={deleteConfirmText}
|
||||
onChange={(event) => setDeleteConfirmText(event.target.value)}
|
||||
placeholder="LOESCHEN"
|
||||
placeholder={deleteWord}
|
||||
disabled={isDeleting}
|
||||
/>
|
||||
<div className={styles.deleteActions}>
|
||||
|
|
@ -251,7 +252,7 @@ export const GDPRPage: React.FC = () => {
|
|||
<button
|
||||
className={styles.dangerButton}
|
||||
onClick={handleDeleteAccount}
|
||||
disabled={isDeleting || deleteConfirmText !== 'LOESCHEN'}
|
||||
disabled={isDeleting || deleteConfirmText !== deleteWord}
|
||||
>
|
||||
{isDeleting ? (
|
||||
<span className={styles.buttonSpinner}>
|
||||
|
|
@ -290,44 +291,34 @@ export const GDPRPage: React.FC = () => {
|
|||
<div className={styles.infoBlock}>
|
||||
<h3>{t('Gesammelte Daten')}</h3>
|
||||
<ul>
|
||||
{Object.entries(consentInfo.dataCollected || {}).map(([key, value]) => (
|
||||
<li key={key}>
|
||||
<strong>{key}:</strong> {value}
|
||||
</li>
|
||||
))}
|
||||
<li><strong>{t('Profil')}:</strong> {t('Name, E-Mail, Benutzername, Spracheinstellungen')}</li>
|
||||
<li><strong>{t('Authentifizierung')}:</strong> {t('Login-Zeitstempel, Authentifizierungsanbieter')}</li>
|
||||
<li><strong>{t('Mitgliedschaften')}:</strong> {t('Mandanten- und Feature-Zugriffseinträge')}</li>
|
||||
<li><strong>{t('Aktivität')}:</strong> {t('Audit-Logs für sicherheitsrelevante Aktionen')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className={styles.infoBlock}>
|
||||
<h3>{t('Verarbeitung')}</h3>
|
||||
<ul>
|
||||
{Object.entries(consentInfo.dataProcessing || {}).map(([key, value]) => (
|
||||
<li key={key}>
|
||||
<strong>{key}:</strong> {value}
|
||||
</li>
|
||||
))}
|
||||
<li><strong>{t('Zweck')}:</strong> {t('Bereitstellung von Multi-Tenant-Plattformdiensten')}</li>
|
||||
<li><strong>{t('Rechtsgrundlage')}:</strong> {t('Vertragserfüllung und berechtigtes Interesse')}</li>
|
||||
<li><strong>{t('Aufbewahrung')}:</strong> {t('Daten werden aufbewahrt, solange das Konto aktiv ist, und bei Kontolöschung gelöscht')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className={styles.infoBlock}>
|
||||
<h3>{t('Ihre Rechte')}</h3>
|
||||
<ul>
|
||||
{Object.entries(consentInfo.userRights || {}).map(([key, value]) => (
|
||||
<li key={key}>
|
||||
<strong>{key}:</strong> {value}
|
||||
</li>
|
||||
))}
|
||||
<li><strong>{t('Auskunft')}:</strong> {t('Datenexport (Artikel 15)')}</li>
|
||||
<li><strong>{t('Datenübertragbarkeit')}:</strong> {t('Portabler Export (Artikel 20)')}</li>
|
||||
<li><strong>{t('Löschung')}:</strong> {t('Kontolöschung (Artikel 17)')}</li>
|
||||
<li><strong>{t('Berichtigung')}:</strong> {t('Profildaten bearbeiten (Artikel 16)')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className={styles.infoBlock}>
|
||||
<h3>{t('Kontakt')}</h3>
|
||||
<ul>
|
||||
{Object.entries({
|
||||
...(consentInfo.contact || {}),
|
||||
email: contactEmail,
|
||||
}).map(([key, value]) => (
|
||||
<li key={key}>
|
||||
<strong>{key}:</strong> {value}
|
||||
</li>
|
||||
))}
|
||||
<li><strong>{t('E-Mail')}:</strong> {contactEmail}</li>
|
||||
<li><strong>{t('Hinweis')}:</strong> {t('Bei Datenschutzanfragen kontaktieren Sie uns bitte mit Ihrer Benutzer-ID.')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ import { useSearchParams } from 'react-router-dom';
|
|||
import { useLanguage } from '../providers/language/LanguageContext';
|
||||
import { useApiRequest } from '../hooks/useApi';
|
||||
import { useConfirm } from '../hooks/useConfirm';
|
||||
import type { RagInventoryDto, RagConnectionDto, RagFeatureInstanceDto } from '../api/connectionApi';
|
||||
import type { RagInventoryDto, RagConnectionDto, RagDataSourceDto, RagFeatureInstanceDto } from '../api/connectionApi';
|
||||
import api from '../api';
|
||||
import { FaDatabase, FaStop, FaSync, FaToggleOn, FaToggleOff, FaRedo, FaExclamationTriangle, FaCheckCircle, FaSlidersH, FaCubes } from 'react-icons/fa';
|
||||
import { mandateDisplayLabel } from '../utils/mandateDisplayUtils';
|
||||
import { DataSourceSettingsModal } from '../components/UnifiedDataBar/DataSourceSettingsModal';
|
||||
|
|
@ -169,6 +170,15 @@ export const RagInventoryPage: React.FC = () => {
|
|||
} catch {}
|
||||
};
|
||||
|
||||
const _handleDsRagToggle = async (connectionId: string, ds: RagDataSourceDto) => {
|
||||
const nodeKey = `ds|${connectionId}|${ds.sourceType}|${ds.path}`;
|
||||
const newValue = !ds.ragIndexEnabled;
|
||||
try {
|
||||
await api.post(`/api/udb/node/${encodeURIComponent(nodeKey)}/flag/ragIndexEnabled`, { value: newValue });
|
||||
_fetchInventory();
|
||||
} catch {}
|
||||
};
|
||||
|
||||
const _handleConsentToggle = async (connectionId: string, currentEnabled: boolean) => {
|
||||
if (currentEnabled) {
|
||||
const ok = await confirm(t('Alle indexierten Inhalte dieser Verbindung werden entfernt. Fortfahren?'), { variant: 'danger', confirmLabel: t('Fortfahren') });
|
||||
|
|
@ -309,6 +319,14 @@ export const RagInventoryPage: React.FC = () => {
|
|||
{t('{f} Dateien · {c} Chunks', { f: conn.totalFiles, c: conn.totalChunks })}
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
className={styles.reindexBtn}
|
||||
onClick={() => _openSettingsForConnection(conn)}
|
||||
title={t('Einstellungen')}
|
||||
style={{ marginLeft: 'auto' }}
|
||||
>
|
||||
<FaSlidersH size={14} />
|
||||
</button>
|
||||
<button
|
||||
className={styles.consentToggle}
|
||||
onClick={() => _handleConsentToggle(conn.id, conn.knowledgeIngestionEnabled)}
|
||||
|
|
@ -425,7 +443,14 @@ export const RagInventoryPage: React.FC = () => {
|
|||
>
|
||||
{ds.fileCount} {t('Dateien')} · {ds.chunkCount} {t('Chunks')}
|
||||
</span>
|
||||
<span className={styles.dsIndex}>{ds.ragIndexEnabled ? '\uD83E\uDDE0' : '\u2014'}</span>
|
||||
<button
|
||||
className={styles.dsIndex}
|
||||
onClick={() => _handleDsRagToggle(conn.id, ds)}
|
||||
title={ds.ragIndexEnabled ? t('Indexierung deaktivieren') : t('Indexierung aktivieren')}
|
||||
style={{ cursor: 'pointer', background: 'none', border: 'none', padding: '2px 4px', fontSize: 'inherit' }}
|
||||
>
|
||||
{ds.ragIndexEnabled ? '\uD83E\uDDE0' : '\u2014'}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
{conn.dataSources.length === 0 && (
|
||||
|
|
|
|||
|
|
@ -63,8 +63,8 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
|||
const [createFeatureCode, setCreateFeatureCode] = useState<string>('');
|
||||
const [createLabel, setCreateLabel] = useState<string>(''); // Label field value
|
||||
|
||||
// Ref to track form data for featureCode detection
|
||||
const formDataRef = useRef<Record<string, any>>({});
|
||||
const _lastTableParams = useRef<any>(undefined);
|
||||
|
||||
// Load features, mandates, and attributes on mount
|
||||
useEffect(() => {
|
||||
|
|
@ -82,12 +82,22 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
|||
}).catch(() => setBackendAttributes([]));
|
||||
}, [fetchFeatures, fetchMandates]);
|
||||
|
||||
// Load instances when mandate changes
|
||||
const _refetchInstances = React.useCallback(async (paginationParams?: any) => {
|
||||
if (!selectedMandateId) return;
|
||||
if (paginationParams && typeof paginationParams === 'object') {
|
||||
_lastTableParams.current = paginationParams;
|
||||
} else {
|
||||
paginationParams = _lastTableParams.current;
|
||||
}
|
||||
if (paginationParams) return fetchInstances(paginationParams);
|
||||
return fetchInstances(selectedMandateId);
|
||||
}, [selectedMandateId, fetchInstances]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedMandateId) {
|
||||
fetchInstances(selectedMandateId);
|
||||
_lastTableParams.current = undefined;
|
||||
}
|
||||
}, [selectedMandateId, fetchInstances]);
|
||||
}, [selectedMandateId]);
|
||||
|
||||
const _rawColumns: ColumnConfig[] = useMemo(() => [
|
||||
{ key: 'label', label: t('Name'), sortable: true, filterable: true, searchable: true, width: 200 },
|
||||
|
|
@ -147,8 +157,8 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
|||
setCreateFeatureCode('');
|
||||
setCreateLabel('');
|
||||
formDataRef.current = {};
|
||||
fetchInstances(selectedMandateId);
|
||||
loadFeatures(); // Refresh global navigation cache
|
||||
_refetchInstances();
|
||||
loadFeatures();
|
||||
showSuccess(t('Feature-Instanz erstellt'), t('Die Instanz "{name}" wurde erfolgreich erstellt.', { name: createLabel }));
|
||||
} else {
|
||||
showError(t('Fehler'), result.error || t('Fehler beim Erstellen der Feature-Instanz'));
|
||||
|
|
@ -185,8 +195,8 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
|||
if (result.success) {
|
||||
setShowEditModal(false);
|
||||
setEditingInstance(null);
|
||||
fetchInstances(selectedMandateId);
|
||||
loadFeatures(); // Refresh global navigation cache
|
||||
_refetchInstances();
|
||||
loadFeatures();
|
||||
showSuccess(t('Feature-Instanz aktualisiert'), t('Die Instanz "{name}" wurde erfolgreich aktualisiert.', { name: data.label }));
|
||||
} else {
|
||||
showError(t('Fehler'), result.error || t('Fehler beim Aktualisieren der Feature-Instanz'));
|
||||
|
|
@ -315,7 +325,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
|||
<div className={styles.headerActions}>
|
||||
<button
|
||||
className={styles.secondaryButton}
|
||||
onClick={() => fetchInstances(selectedMandateId)}
|
||||
onClick={() => _refetchInstances()}
|
||||
disabled={loading}
|
||||
>
|
||||
<FaSync className={loading ? 'spinning' : ''} /> {t('Aktualisieren')}
|
||||
|
|
@ -426,7 +436,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
|||
}
|
||||
]}
|
||||
hookData={{
|
||||
refetch: fetchInstances,
|
||||
refetch: _refetchInstances,
|
||||
pagination: instancesPagination,
|
||||
handleDelete: handleDeleteInstance,
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
* Allows adding, removing, and updating user roles within feature instances.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react';
|
||||
import { useFeatureAccess, type FeatureAccessUser, type FeatureInstanceRole, type PaginationParams, type PaginationMetadata } from '../../hooks/useFeatureAccess';
|
||||
import { useUserMandates } from '../../hooks/useUserMandates';
|
||||
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
|
||||
|
|
@ -70,6 +70,7 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
|||
const [, setIsSubmitting] = useState(false);
|
||||
const [usersLoading, setUsersLoading] = useState(false);
|
||||
const [usersPagination, setUsersPagination] = useState<PaginationMetadata | null>(null);
|
||||
const _lastTableParams = useRef<any>(undefined);
|
||||
|
||||
// Extract mandateId and instanceId from combined key
|
||||
const selectedMandateId = useMemo(() => {
|
||||
|
|
@ -137,21 +138,13 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
|||
loadCombinedOptions();
|
||||
}, [fetchFeatures, fetchMandates]);
|
||||
|
||||
// Load users and roles when instance changes
|
||||
useEffect(() => {
|
||||
if (selectedMandateId && selectedInstanceId) {
|
||||
setUsersLoading(true);
|
||||
Promise.all([
|
||||
fetchInstanceUsers(selectedMandateId, selectedInstanceId),
|
||||
fetchInstanceRoles(selectedMandateId, selectedInstanceId),
|
||||
]).then(([users, roles]) => {
|
||||
setInstanceUsers(users);
|
||||
setInstanceRoles(roles);
|
||||
}).finally(() => {
|
||||
setUsersLoading(false);
|
||||
});
|
||||
_lastTableParams.current = undefined;
|
||||
fetchInstanceRoles(selectedMandateId, selectedInstanceId)
|
||||
.then(setInstanceRoles);
|
||||
}
|
||||
}, [selectedMandateId, selectedInstanceId, fetchInstanceUsers, fetchInstanceRoles]);
|
||||
}, [selectedMandateId, selectedInstanceId, fetchInstanceRoles]);
|
||||
|
||||
// Load mandate members for the add modal (only users who are members of the selected mandate)
|
||||
useEffect(() => {
|
||||
|
|
@ -172,12 +165,15 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
|||
}).catch(() => setAllUsers([]));
|
||||
}, [selectedMandateId]);
|
||||
|
||||
// Refresh instance users with optional pagination
|
||||
const refreshUsers = useCallback(async (paginationParams?: PaginationParams) => {
|
||||
if (selectedMandateId && selectedInstanceId) {
|
||||
if (paginationParams && Object.keys(paginationParams).length > 0) {
|
||||
_lastTableParams.current = paginationParams;
|
||||
} else {
|
||||
paginationParams = _lastTableParams.current;
|
||||
}
|
||||
setUsersLoading(true);
|
||||
try {
|
||||
// Build query params
|
||||
const params = new URLSearchParams();
|
||||
if (paginationParams && Object.keys(paginationParams).length > 0) {
|
||||
params.append('pagination', JSON.stringify(paginationParams));
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
* - Actions: Create feature role, edit description, manage AccessRules, delete role
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react';
|
||||
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
|
||||
import { FormGeneratorForm, type AttributeDefinition } from '../../components/FormGenerator/FormGeneratorForm';
|
||||
import { AccessRulesEditor } from '../../components/AccessRules';
|
||||
|
|
@ -66,6 +66,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
|||
const [editingRole, setEditingRole] = useState<FeatureRole | null>(null);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [permissionsRole, setPermissionsRole] = useState<FeatureRole | null>(null);
|
||||
const _lastTableParams = useRef<any>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
fetchAttributes(request, 'Role')
|
||||
|
|
@ -96,12 +97,13 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
|||
|
||||
const [pagination, setPagination] = useState<any>(null);
|
||||
|
||||
// Load roles when feature changes
|
||||
const fetchRoles = useCallback(async (params?: any) => {
|
||||
if (!selectedFeatureCode) {
|
||||
setRoles([]);
|
||||
return;
|
||||
}
|
||||
if (params) _lastTableParams.current = params;
|
||||
else params = _lastTableParams.current;
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
* Allows creating, viewing, and revoking invitations.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react';
|
||||
import { useInvitations, type Invitation, type InvitationCreate } from '../../hooks/useInvitations';
|
||||
import { useUserMandates, type Mandate, type Role } from '../../hooks/useUserMandates';
|
||||
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
|
||||
|
|
@ -53,6 +53,7 @@ export const AdminInvitationsPage: React.FC = () => {
|
|||
const [_isSubmitting, setIsSubmitting] = useState(false); // Prefixed with _ to suppress warning
|
||||
const [copySuccess, setCopySuccess] = useState(false);
|
||||
const [backendAttributes, setBackendAttributes] = useState<AttributeDefinition[]>([]);
|
||||
const _lastTableParams = useRef<any>(undefined);
|
||||
|
||||
// Load mandates and attributes on mount
|
||||
useEffect(() => {
|
||||
|
|
@ -69,13 +70,24 @@ export const AdminInvitationsPage: React.FC = () => {
|
|||
.catch(() => setBackendAttributes([]));
|
||||
}, [fetchMandates, request]);
|
||||
|
||||
// Load invitations and roles when mandate changes (same roles as AdminUserMandatesPage: user, viewer, admin)
|
||||
const _refetchInvitations = useCallback(async (paginationParams?: any) => {
|
||||
if (!selectedMandateId) return;
|
||||
if (paginationParams && typeof paginationParams === 'object' && !Array.isArray(paginationParams) && typeof paginationParams !== 'string') {
|
||||
_lastTableParams.current = paginationParams;
|
||||
} else {
|
||||
paginationParams = _lastTableParams.current;
|
||||
}
|
||||
if (paginationParams) return fetchInvitations(paginationParams, { includeExpired: showExpired, includeUsed: showUsed });
|
||||
return fetchInvitations(selectedMandateId, { includeExpired: showExpired, includeUsed: showUsed });
|
||||
}, [selectedMandateId, showExpired, showUsed, fetchInvitations]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedMandateId) {
|
||||
fetchInvitations(selectedMandateId, { includeExpired: showExpired, includeUsed: showUsed });
|
||||
_lastTableParams.current = undefined;
|
||||
_refetchInvitations();
|
||||
fetchRoles(selectedMandateId).then(setRoles);
|
||||
}
|
||||
}, [selectedMandateId, showExpired, showUsed, fetchInvitations, fetchRoles]);
|
||||
}, [selectedMandateId, showExpired, showUsed, _refetchInvitations, fetchRoles]);
|
||||
|
||||
// Format timestamp (used by URL modal only).
|
||||
const formatDate = (timestamp: number) => {
|
||||
|
|
@ -165,7 +177,7 @@ export const AdminInvitationsPage: React.FC = () => {
|
|||
if (result.success && result.data) {
|
||||
setShowCreateModal(false);
|
||||
setShowUrlModal(result.data);
|
||||
fetchInvitations(selectedMandateId, { includeExpired: showExpired, includeUsed: showUsed });
|
||||
_refetchInvitations();
|
||||
} else {
|
||||
showError(t('Fehler'), result.error || t('Fehler beim Erstellen der Einladung'));
|
||||
}
|
||||
|
|
@ -276,7 +288,7 @@ export const AdminInvitationsPage: React.FC = () => {
|
|||
<div className={styles.headerActions}>
|
||||
<button
|
||||
className={styles.secondaryButton}
|
||||
onClick={() => fetchInvitations(selectedMandateId, { includeExpired: showExpired, includeUsed: showUsed })}
|
||||
onClick={() => _refetchInvitations()}
|
||||
disabled={loading}
|
||||
>
|
||||
<FaSync className={loading ? 'spinning' : ''} /> {t('Aktualisieren')}
|
||||
|
|
@ -332,7 +344,7 @@ export const AdminInvitationsPage: React.FC = () => {
|
|||
]}
|
||||
hookData={{
|
||||
handleDelete: handleDeleteInvitation,
|
||||
refetch: (params?: any) => fetchInvitations(params || selectedMandateId, { includeExpired: showExpired, includeUsed: showUsed }),
|
||||
refetch: _refetchInvitations,
|
||||
pagination,
|
||||
}}
|
||||
emptyMessage={t('Keine Einladungen gefunden')}
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ export const AdminMandateRolesPage: React.FC = () => {
|
|||
// Store current filter state for refetch
|
||||
const currentScopeFilterRef = useRef(scopeFilter);
|
||||
currentScopeFilterRef.current = scopeFilter;
|
||||
const _lastTableParams = useRef<any>(undefined);
|
||||
|
||||
// Load mandates and attributes on mount
|
||||
useEffect(() => {
|
||||
|
|
@ -81,24 +82,25 @@ export const AdminMandateRolesPage: React.FC = () => {
|
|||
.catch(() => setBackendAttributes([]));
|
||||
}, [fetchMandates, request]);
|
||||
|
||||
// Load roles when mandate or scopeFilter changes
|
||||
useEffect(() => {
|
||||
if (selectedMandateId) {
|
||||
fetchRoles(selectedMandateId, { scopeFilter });
|
||||
}
|
||||
}, [selectedMandateId, scopeFilter, fetchRoles]);
|
||||
|
||||
// Refetch wrapper that accepts pagination params from FormGeneratorTable
|
||||
// and includes the current mandateId and scopeFilter
|
||||
const refetchWithParams = useCallback(async (paginationParams?: PaginationParams) => {
|
||||
if (!selectedMandateId) return;
|
||||
// Merge pagination params with current filter state
|
||||
if (paginationParams && Object.keys(paginationParams).length > 0) {
|
||||
_lastTableParams.current = paginationParams;
|
||||
} else {
|
||||
paginationParams = _lastTableParams.current;
|
||||
}
|
||||
return fetchRoles(selectedMandateId, {
|
||||
...paginationParams,
|
||||
scopeFilter: currentScopeFilterRef.current
|
||||
});
|
||||
}, [selectedMandateId, fetchRoles]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedMandateId) {
|
||||
refetchWithParams();
|
||||
}
|
||||
}, [selectedMandateId, scopeFilter, refetchWithParams]);
|
||||
|
||||
const getDescriptionText = (desc: any) => {
|
||||
if (!desc) return '-';
|
||||
if (typeof desc === 'string') return desc;
|
||||
|
|
@ -183,7 +185,7 @@ export const AdminMandateRolesPage: React.FC = () => {
|
|||
|
||||
if (result.success) {
|
||||
setShowCreateModal(false);
|
||||
await fetchRoles(selectedMandateId, { scopeFilter });
|
||||
await refetchWithParams();
|
||||
} else {
|
||||
showError(t('Fehler'), result.error || t('Fehler beim Erstellen der Rolle'));
|
||||
}
|
||||
|
|
@ -212,9 +214,7 @@ export const AdminMandateRolesPage: React.FC = () => {
|
|||
|
||||
if (result.success) {
|
||||
setEditingRole(null);
|
||||
if (selectedMandateId) {
|
||||
await fetchRoles(selectedMandateId, { scopeFilter });
|
||||
}
|
||||
await refetchWithParams();
|
||||
} else {
|
||||
showError(t('Fehler'), result.error || t('Fehler beim Aktualisieren der Rolle'));
|
||||
}
|
||||
|
|
@ -235,8 +235,7 @@ export const AdminMandateRolesPage: React.FC = () => {
|
|||
|
||||
const result = await deleteRole(role.id);
|
||||
if (result.success) {
|
||||
// Refetch to update the list
|
||||
await fetchRoles(selectedMandateId, { scopeFilter });
|
||||
await refetchWithParams();
|
||||
} else {
|
||||
showError(t('Fehler'), result.error || t('Fehler beim Löschen der Rolle'));
|
||||
}
|
||||
|
|
@ -332,7 +331,7 @@ export const AdminMandateRolesPage: React.FC = () => {
|
|||
<div className={styles.headerActions}>
|
||||
<button
|
||||
className={styles.secondaryButton}
|
||||
onClick={() => fetchRoles(selectedMandateId, { scopeFilter })}
|
||||
onClick={() => refetchWithParams()}
|
||||
disabled={loading}
|
||||
>
|
||||
<FaSync className={loading ? 'spinning' : ''} /> {t('Aktualisieren')}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
* Admin page for managing Mandates (tenants) using FormGeneratorTable.
|
||||
*/
|
||||
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import React, { useState, useMemo, useCallback, useRef } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAdminMandates, useMandateFormAttributes, type Mandate } from '../../hooks/useMandates';
|
||||
import { useApiRequest } from '../../hooks/useApi';
|
||||
|
|
@ -60,6 +60,12 @@ export const AdminMandatesPage: React.FC = () => {
|
|||
} = useMandateFormAttributes();
|
||||
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
const _lastTableParams = useRef<any>(undefined);
|
||||
const _tableRefetch = useCallback(async (paginationParams?: any) => {
|
||||
if (paginationParams) _lastTableParams.current = paginationParams;
|
||||
else paginationParams = _lastTableParams.current;
|
||||
return refetch(paginationParams);
|
||||
}, [refetch]);
|
||||
/** Mandate row merged with billing fields for FormGenerator */
|
||||
const [editingFormData, setEditingFormData] = useState<Record<string, unknown> | null>(null);
|
||||
const [editingBillingWarning, setEditingBillingWarning] = useState<string | null>(null);
|
||||
|
|
@ -192,7 +198,7 @@ export const AdminMandatesPage: React.FC = () => {
|
|||
<p className={styles.errorMessage}>
|
||||
{t('Fehler beim Laden der Mandanten')}: {error}
|
||||
</p>
|
||||
<button className={styles.secondaryButton} onClick={() => refetch()}>
|
||||
<button className={styles.secondaryButton} onClick={() => _tableRefetch()}>
|
||||
<FaSync /> {t('Erneut versuchen')}
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -229,7 +235,7 @@ export const AdminMandatesPage: React.FC = () => {
|
|||
</button>
|
||||
<button
|
||||
className={styles.secondaryButton}
|
||||
onClick={() => refetch()}
|
||||
onClick={() => _tableRefetch()}
|
||||
disabled={loading}
|
||||
>
|
||||
<FaSync className={loading ? 'spinning' : ''} /> {t('Aktualisieren')}
|
||||
|
|
@ -282,7 +288,7 @@ export const AdminMandatesPage: React.FC = () => {
|
|||
}] : []}
|
||||
onDelete={handleDeleteMandate}
|
||||
hookData={{
|
||||
refetch,
|
||||
refetch: _tableRefetch,
|
||||
permissions,
|
||||
pagination,
|
||||
handleDelete,
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ export const AdminUserMandatesPage: React.FC = () => {
|
|||
|
||||
// Store current mandateId for refetch
|
||||
const currentMandateIdRef = useRef<string>('');
|
||||
const _lastTableParams = useRef<any>(undefined);
|
||||
|
||||
// State
|
||||
const [mandates, setMandates] = useState<Mandate[]>([]);
|
||||
|
|
@ -72,23 +73,23 @@ export const AdminUserMandatesPage: React.FC = () => {
|
|||
.catch(() => setBackendAttributes([]));
|
||||
}, [fetchMandates, request]);
|
||||
|
||||
// Load users when mandate changes
|
||||
// Load roles when mandate changes — table auto-reloads via filterScopeKey
|
||||
useEffect(() => {
|
||||
if (selectedMandateId) {
|
||||
currentMandateIdRef.current = selectedMandateId;
|
||||
fetchMandateUsers(selectedMandateId);
|
||||
_lastTableParams.current = undefined;
|
||||
fetchRoles(selectedMandateId).then(setRoles);
|
||||
}
|
||||
}, [selectedMandateId, fetchMandateUsers, fetchRoles]);
|
||||
}, [selectedMandateId, fetchRoles]);
|
||||
|
||||
// Refetch wrapper that accepts pagination params from FormGeneratorTable
|
||||
const refetchWithParams = useCallback(async (paginationParams?: PaginationParams) => {
|
||||
const mandateId = currentMandateIdRef.current;
|
||||
if (!mandateId) return;
|
||||
// If pagination params provided, pass them; otherwise just use mandateId
|
||||
if (paginationParams && Object.keys(paginationParams).length > 0) {
|
||||
_lastTableParams.current = paginationParams;
|
||||
return fetchMandateUsers(paginationParams);
|
||||
}
|
||||
if (_lastTableParams.current) return fetchMandateUsers(_lastTableParams.current);
|
||||
return fetchMandateUsers(mandateId);
|
||||
}, [fetchMandateUsers]);
|
||||
|
||||
|
|
@ -214,7 +215,7 @@ export const AdminUserMandatesPage: React.FC = () => {
|
|||
const result = await addUserToMandate(selectedMandateId, data);
|
||||
if (result.success) {
|
||||
setShowAddModal(false);
|
||||
fetchMandateUsers(selectedMandateId);
|
||||
refetchWithParams();
|
||||
} else {
|
||||
showError(t('Fehler'), result.error || t('Fehler beim Hinzufügen des Benutzers'));
|
||||
}
|
||||
|
|
@ -231,7 +232,7 @@ export const AdminUserMandatesPage: React.FC = () => {
|
|||
const result = await updateUserRoles(selectedMandateId, editingUser.userId, data.roleIds);
|
||||
if (result.success) {
|
||||
setEditingUser(null);
|
||||
fetchMandateUsers(selectedMandateId);
|
||||
refetchWithParams();
|
||||
} else {
|
||||
showError(t('Fehler'), result.error || t('Fehler beim Aktualisieren der Rollen'));
|
||||
}
|
||||
|
|
@ -309,7 +310,7 @@ export const AdminUserMandatesPage: React.FC = () => {
|
|||
<div className={styles.headerActions}>
|
||||
<button
|
||||
className={styles.secondaryButton}
|
||||
onClick={() => fetchMandateUsers(selectedMandateId)}
|
||||
onClick={() => refetchWithParams()}
|
||||
disabled={loading}
|
||||
>
|
||||
<FaSync className={loading ? 'spinning' : ''} /> {t('Aktualisieren')}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
* Admin page for managing Users using FormGeneratorTable.
|
||||
*/
|
||||
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import React, { useState, useMemo, useCallback, useRef } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useOrgUsers, useUserOperations } from '../../hooks/useUsers';
|
||||
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
|
||||
|
|
@ -61,6 +61,12 @@ export const AdminUsersPage: React.FC = () => {
|
|||
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
const [editingUser, setEditingUser] = useState<User | null>(null);
|
||||
const _lastTableParams = useRef<any>(undefined);
|
||||
const _tableRefetch = useCallback(async (paginationParams?: any) => {
|
||||
if (paginationParams) _lastTableParams.current = paginationParams;
|
||||
else paginationParams = _lastTableParams.current;
|
||||
return refetch(paginationParams);
|
||||
}, [refetch]);
|
||||
|
||||
// Generate columns from attributes; types from backend via resolveColumnTypes
|
||||
const columns = useMemo(() => {
|
||||
|
|
@ -96,7 +102,7 @@ export const AdminUsersPage: React.FC = () => {
|
|||
const result = await createUser(data as Omit<User, 'id'>);
|
||||
if (result.success) {
|
||||
setShowCreateModal(false);
|
||||
refetch(); // Refresh the list
|
||||
_tableRefetch();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -106,7 +112,7 @@ export const AdminUsersPage: React.FC = () => {
|
|||
const result = await updateUser(editingUser.id, data);
|
||||
if (result.success) {
|
||||
setEditingUser(null);
|
||||
refetch(); // Refresh the list
|
||||
_tableRefetch();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -114,7 +120,7 @@ export const AdminUsersPage: React.FC = () => {
|
|||
const handleDeleteUser = async (user: User) => {
|
||||
const success = await deleteUser(user.id);
|
||||
if (success) {
|
||||
refetch(); // Refresh the list
|
||||
_tableRefetch();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -169,7 +175,7 @@ export const AdminUsersPage: React.FC = () => {
|
|||
<p className={styles.errorMessage}>
|
||||
{t('Fehler beim Laden der Benutzer')}: {error}
|
||||
</p>
|
||||
<button className={styles.secondaryButton} onClick={() => refetch()}>
|
||||
<button className={styles.secondaryButton} onClick={() => _tableRefetch()}>
|
||||
<FaSync /> {t('Erneut versuchen')}
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -207,7 +213,7 @@ export const AdminUsersPage: React.FC = () => {
|
|||
</button>
|
||||
<button
|
||||
className={styles.secondaryButton}
|
||||
onClick={() => refetch()}
|
||||
onClick={() => _tableRefetch()}
|
||||
disabled={loading}
|
||||
>
|
||||
<FaSync className={loading ? 'spinning' : ''} /> {t('Aktualisieren')}
|
||||
|
|
@ -257,7 +263,7 @@ export const AdminUsersPage: React.FC = () => {
|
|||
] : []}
|
||||
onDelete={handleDeleteUser}
|
||||
hookData={{
|
||||
refetch,
|
||||
refetch: _tableRefetch,
|
||||
permissions,
|
||||
pagination,
|
||||
handleDelete: deleteUser,
|
||||
|
|
|
|||
|
|
@ -126,7 +126,10 @@ export const AdminInvitationWizardPage: React.FC = () => {
|
|||
// ==========================================================================
|
||||
|
||||
useEffect(() => {
|
||||
fetchMandates().then(setMandates);
|
||||
fetchMandates().then((data: Mandate[]) => {
|
||||
data.sort((a, b) => getMandateName(a).localeCompare(getMandateName(b), undefined, { sensitivity: 'base' }));
|
||||
setMandates(data);
|
||||
});
|
||||
}, [fetchMandates]);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -144,14 +147,18 @@ export const AdminInvitationWizardPage: React.FC = () => {
|
|||
if (inviteType === 'mandate') {
|
||||
fetchRoles(selectedMandate.id).then((r: Role[]) => {
|
||||
const mandateRoles = r.filter((role: Role) => !role.featureInstanceId);
|
||||
setRoles(mandateRoles.map((role: Role) => ({ id: role.id, roleLabel: role.roleLabel })));
|
||||
const mapped = mandateRoles.map((role: Role) => ({ id: role.id, roleLabel: role.roleLabel }));
|
||||
mapped.sort((a, b) => a.roleLabel.localeCompare(b.roleLabel, undefined, { sensitivity: 'base' }));
|
||||
setRoles(mapped);
|
||||
});
|
||||
fetchMandateUsers(selectedMandate.id).then((users: { userId: string }[]) => {
|
||||
setExistingAccessUserIds(new Set(users.map(u => u.userId)));
|
||||
}).catch(() => setExistingAccessUserIds(new Set()));
|
||||
} else if (inviteType === 'featureInstance' && selectedInstance) {
|
||||
fetchInstanceRoles(selectedMandate.id, selectedInstance.id).then((r: FeatureInstanceRole[]) => {
|
||||
setRoles(r.map(role => ({ id: role.id, roleLabel: role.roleLabel })));
|
||||
const mapped = r.map(role => ({ id: role.id, roleLabel: role.roleLabel }));
|
||||
mapped.sort((a, b) => a.roleLabel.localeCompare(b.roleLabel, undefined, { sensitivity: 'base' }));
|
||||
setRoles(mapped);
|
||||
});
|
||||
fetchInstanceUsers(selectedMandate.id, selectedInstance.id).then((users: { userId: string }[]) => {
|
||||
setExistingAccessUserIds(new Set(users.map(u => u.userId)));
|
||||
|
|
@ -160,7 +167,10 @@ export const AdminInvitationWizardPage: React.FC = () => {
|
|||
setRoles([]);
|
||||
setExistingAccessUserIds(new Set());
|
||||
}
|
||||
fetchAllUsers().then(setAllSystemUsers);
|
||||
fetchAllUsers().then((data: any[]) => {
|
||||
data.sort((a: any, b: any) => (a.username || '').localeCompare(b.username || '', undefined, { sensitivity: 'base' }));
|
||||
setAllSystemUsers(data);
|
||||
});
|
||||
}
|
||||
}, [step, selectedMandate, selectedInstance, inviteType, fetchRoles, fetchInstanceRoles, fetchAllUsers, fetchMandateUsers, fetchInstanceUsers]);
|
||||
|
||||
|
|
|
|||
|
|
@ -129,6 +129,7 @@ export const AdminMandateWizardPage: React.FC = () => {
|
|||
const loadMandates = useCallback(async () => {
|
||||
try {
|
||||
const data = await fetchMandatesList();
|
||||
data.sort((a: Mandate, b: Mandate) => getMandateName(a).localeCompare(getMandateName(b), undefined, { sensitivity: 'base' }));
|
||||
setMandates(data);
|
||||
} catch {
|
||||
setError(t('Fehler beim Laden der Mandanten'));
|
||||
|
|
@ -162,13 +163,16 @@ export const AdminMandateWizardPage: React.FC = () => {
|
|||
|
||||
const loadAllSystemUsers = useCallback(async () => {
|
||||
const data = await fetchAllUsers();
|
||||
data.sort((a: any, b: any) => getUserDisplayName(a).localeCompare(getUserDisplayName(b), undefined, { sensitivity: 'base' }));
|
||||
setAllSystemUsers(data);
|
||||
}, [fetchAllUsers]);
|
||||
|
||||
const loadMandateRoles = useCallback(async () => {
|
||||
if (!selectedMandate) return;
|
||||
const data = await fetchMandateRolesList(selectedMandate.id);
|
||||
setMandateRoles(data.map((r: Role) => ({ id: r.id, roleLabel: r.roleLabel })));
|
||||
const mapped = data.map((r: Role) => ({ id: r.id, roleLabel: r.roleLabel }));
|
||||
mapped.sort((a: RoleOption, b: RoleOption) => a.roleLabel.localeCompare(b.roleLabel, undefined, { sensitivity: 'base' }));
|
||||
setMandateRoles(mapped);
|
||||
}, [selectedMandate, fetchMandateRolesList]);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -200,7 +204,9 @@ export const AdminMandateWizardPage: React.FC = () => {
|
|||
const loadInstanceRoles = useCallback(async () => {
|
||||
if (!selectedInstance || !selectedMandate) return;
|
||||
const data = await fetchInstanceRolesList(selectedMandate.id, selectedInstance.id);
|
||||
setInstanceRoles(data.map((r: FeatureInstanceRole) => ({ id: r.id, roleLabel: r.roleLabel })));
|
||||
const mapped = data.map((r: FeatureInstanceRole) => ({ id: r.id, roleLabel: r.roleLabel }));
|
||||
mapped.sort((a: RoleOption, b: RoleOption) => a.roleLabel.localeCompare(b.roleLabel, undefined, { sensitivity: 'base' }));
|
||||
setInstanceRoles(mapped);
|
||||
}, [selectedInstance, selectedMandate, fetchInstanceRolesList]);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -95,17 +95,19 @@ export const FeatureInstanceWizard: React.FC<FeatureInstanceWizardProps> = ({ ma
|
|||
fetchInstanceRoles(mandateId, createdInstanceId),
|
||||
api.get(`/api/mandates/${mandateId}/users`),
|
||||
]);
|
||||
setInstanceRoles(Array.isArray(roleList) ? roleList : []);
|
||||
const roles = Array.isArray(roleList) ? [...roleList] : [];
|
||||
roles.sort((a: { roleLabel: string }, b: { roleLabel: string }) => a.roleLabel.localeCompare(b.roleLabel, undefined, { sensitivity: 'base' }));
|
||||
setInstanceRoles(roles);
|
||||
const data = usersRes.data?.items || usersRes.data || [];
|
||||
setMandateUsers(
|
||||
Array.isArray(data)
|
||||
const users = Array.isArray(data)
|
||||
? data.map((u: { userId: string; username: string; email?: string }) => ({
|
||||
id: u.userId,
|
||||
username: u.username,
|
||||
email: u.email,
|
||||
}))
|
||||
: []
|
||||
);
|
||||
: [];
|
||||
users.sort((a: { username: string }, b: { username: string }) => a.username.localeCompare(b.username, undefined, { sensitivity: 'base' }));
|
||||
setMandateUsers(users);
|
||||
} catch {
|
||||
setInstanceRoles([]);
|
||||
setMandateUsers([]);
|
||||
|
|
@ -183,7 +185,7 @@ export const FeatureInstanceWizard: React.FC<FeatureInstanceWizardProps> = ({ ma
|
|||
<p className={wizardStyles.fieldHint}>{t('Keine Mandanten verfügbar')}</p>
|
||||
) : (
|
||||
<div className={wizardStyles.cardGrid}>
|
||||
{mandates.map((m) => {
|
||||
{[...mandates].sort((a, b) => getMandateName(a).localeCompare(getMandateName(b), undefined, { sensitivity: 'base' })).map((m) => {
|
||||
const isActive = mandateId === m.id;
|
||||
return (
|
||||
<button
|
||||
|
|
@ -209,7 +211,7 @@ export const FeatureInstanceWizard: React.FC<FeatureInstanceWizardProps> = ({ ma
|
|||
<p className={wizardStyles.fieldHint}>{t('Keine Features verfügbar')}</p>
|
||||
) : (
|
||||
<div className={wizardStyles.cardGrid}>
|
||||
{features.map((f) => {
|
||||
{[...features].sort((a, b) => (a.label || a.code).localeCompare(b.label || b.code, undefined, { sensitivity: 'base' })).map((f) => {
|
||||
const isActive = featureCode === f.code;
|
||||
return (
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
* Follows the pattern established in AdminUsersPage/WorkflowsPage.
|
||||
*/
|
||||
|
||||
import React, { useState, useMemo, useEffect, useRef } from 'react';
|
||||
import React, { useState, useMemo, useEffect, useRef, useCallback } from 'react';
|
||||
import { useConnections, type Connection } from '../../hooks/useConnections';
|
||||
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
|
||||
import { FormGeneratorForm } from '../../components/FormGenerator/FormGeneratorForm';
|
||||
|
|
@ -63,7 +63,12 @@ export const ConnectionsPage: React.FC = () => {
|
|||
const [wizardOpen, setWizardOpen] = useState(false);
|
||||
|
||||
const { request } = useApiRequest();
|
||||
// Banner shown while knowledge bootstrap is running in the background
|
||||
const _lastTableParams = useRef<any>(undefined);
|
||||
const _tableRefetch = useCallback(async (paginationParams?: any) => {
|
||||
if (paginationParams) _lastTableParams.current = paginationParams;
|
||||
else paginationParams = _lastTableParams.current;
|
||||
return refetch(paginationParams);
|
||||
}, [refetch]);
|
||||
const [syncBanner, setSyncBanner] = useState<{
|
||||
connector: string;
|
||||
startedAt: number;
|
||||
|
|
@ -80,9 +85,8 @@ export const ConnectionsPage: React.FC = () => {
|
|||
setSyncBanner(null);
|
||||
};
|
||||
|
||||
// Initial fetch
|
||||
useEffect(() => {
|
||||
refetch();
|
||||
_tableRefetch();
|
||||
}, []);
|
||||
|
||||
// Generate columns from attributes - hide internal/redundant fields
|
||||
|
|
@ -151,7 +155,7 @@ export const ConnectionsPage: React.FC = () => {
|
|||
|
||||
await handleInlineUpdate(editingConnection.id, updateData, editingConnection);
|
||||
setEditingConnection(null);
|
||||
refetch();
|
||||
_tableRefetch();
|
||||
} catch (error) {
|
||||
console.error('Error updating connection:', error);
|
||||
}
|
||||
|
|
@ -162,7 +166,7 @@ export const ConnectionsPage: React.FC = () => {
|
|||
setDeletingConnections(prev => new Set(prev).add(connection.id));
|
||||
try {
|
||||
await deleteConnection(connection.id);
|
||||
refetch();
|
||||
_tableRefetch();
|
||||
} catch (error) {
|
||||
console.error('Error deleting connection:', error);
|
||||
} finally {
|
||||
|
|
@ -178,7 +182,7 @@ export const ConnectionsPage: React.FC = () => {
|
|||
const handleConnect = async (connection: Connection) => {
|
||||
try {
|
||||
await connectWithPopup(connection.id);
|
||||
refetch();
|
||||
_tableRefetch();
|
||||
} catch (error) {
|
||||
console.error('Error connecting:', error);
|
||||
}
|
||||
|
|
@ -193,7 +197,7 @@ export const ConnectionsPage: React.FC = () => {
|
|||
} else if (connection.authority === 'google') {
|
||||
await refreshGoogleToken(connection.id);
|
||||
}
|
||||
refetch();
|
||||
_tableRefetch();
|
||||
} catch (error) {
|
||||
console.error('Error refreshing token:', error);
|
||||
} finally {
|
||||
|
|
@ -211,7 +215,7 @@ export const ConnectionsPage: React.FC = () => {
|
|||
setReconnectingConnections(prev => new Set(prev).add(connection.id));
|
||||
try {
|
||||
await connectWithPopup(connection.id, true);
|
||||
refetch();
|
||||
_tableRefetch();
|
||||
} catch (error) {
|
||||
console.error('Error reconnecting:', error);
|
||||
} finally {
|
||||
|
|
@ -230,7 +234,7 @@ export const ConnectionsPage: React.FC = () => {
|
|||
) => {
|
||||
try {
|
||||
await createConnectionAndAuth(type, knowledgeEnabled, knowledgePreferences ?? null);
|
||||
refetch();
|
||||
_tableRefetch();
|
||||
if (knowledgeEnabled) {
|
||||
const LABELS: Record<ConnectorType, string> = { google: 'Google', msft: 'Microsoft 365', clickup: 'ClickUp', infomaniak: 'Infomaniak' };
|
||||
showSyncBanner(LABELS[type] ?? type);
|
||||
|
|
@ -244,7 +248,7 @@ export const ConnectionsPage: React.FC = () => {
|
|||
try {
|
||||
const newConn = await createInfomaniakConnection();
|
||||
await submitInfomaniakToken(newConn.id, token);
|
||||
refetch();
|
||||
_tableRefetch();
|
||||
if (knowledgeEnabled) showSyncBanner('Infomaniak');
|
||||
} catch (error) {
|
||||
console.error('Error creating Infomaniak connection:', error);
|
||||
|
|
@ -266,7 +270,7 @@ export const ConnectionsPage: React.FC = () => {
|
|||
setTogglingConsent(prev => new Set(prev).add(connection.id));
|
||||
try {
|
||||
await patchKnowledgeConsent(request, connection.id, newEnabled);
|
||||
await refetch();
|
||||
await _tableRefetch();
|
||||
} catch (error) {
|
||||
console.error('Error toggling knowledge consent:', error);
|
||||
} finally {
|
||||
|
|
@ -298,7 +302,7 @@ export const ConnectionsPage: React.FC = () => {
|
|||
<div className={styles.errorContainer}>
|
||||
<span className={styles.errorIcon}>⚠️</span>
|
||||
<p className={styles.errorMessage}>{t('Fehler beim Laden der Verbindungen: {detail}', { detail: String(error) })}</p>
|
||||
<button className={styles.secondaryButton} onClick={() => refetch()}>
|
||||
<button className={styles.secondaryButton} onClick={() => _tableRefetch()}>
|
||||
<FaSync /> {t('Erneut versuchen')}
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -324,7 +328,7 @@ export const ConnectionsPage: React.FC = () => {
|
|||
<div className={styles.headerActions}>
|
||||
<button
|
||||
className={styles.secondaryButton}
|
||||
onClick={() => refetch()}
|
||||
onClick={() => _tableRefetch()}
|
||||
disabled={loading}
|
||||
>
|
||||
<FaSync className={loading ? 'spinning' : ''} /> {t('Aktualisieren')}
|
||||
|
|
@ -444,7 +448,7 @@ export const ConnectionsPage: React.FC = () => {
|
|||
]}
|
||||
onDelete={handleDelete}
|
||||
hookData={{
|
||||
refetch,
|
||||
refetch: _tableRefetch,
|
||||
permissions,
|
||||
pagination,
|
||||
groupLayout,
|
||||
|
|
|
|||
|
|
@ -100,7 +100,10 @@ export const FilesPage: React.FC = () => {
|
|||
const [treeVisible, setTreeVisible] = useState(true);
|
||||
const [tableVisible, setTableVisible] = useState(true);
|
||||
|
||||
const _lastTableParams = useRef<any>(undefined);
|
||||
const _tableRefetch = useCallback(async (params?: any) => {
|
||||
if (params) _lastTableParams.current = params;
|
||||
else params = _lastTableParams.current;
|
||||
const nextParams = { ...(params || {}) };
|
||||
const nextFilters = { ...(nextParams.filters || {}) };
|
||||
const normalizedFolderId = normalizeFolderFilterId(selectedFolderId);
|
||||
|
|
@ -172,7 +175,7 @@ export const FilesPage: React.FC = () => {
|
|||
);
|
||||
|
||||
const _refreshAll = useCallback(async () => {
|
||||
await _tableRefetch({ page: 1, pageSize: 25 });
|
||||
await _tableRefetch();
|
||||
setTreeKey(k => k + 1);
|
||||
}, [_tableRefetch]);
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
* Follows the pattern established in AdminUsersPage/WorkflowsPage.
|
||||
*/
|
||||
|
||||
import React, { useState, useMemo, useEffect, useCallback } from 'react';
|
||||
import React, { useState, useMemo, useEffect, useCallback, useRef } from 'react';
|
||||
import { usePrompts, usePromptOperations } from '../../hooks/usePrompts';
|
||||
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
|
||||
import { FormGeneratorForm } from '../../components/FormGenerator/FormGeneratorForm';
|
||||
|
|
@ -58,16 +58,15 @@ export const PromptsPage: React.FC = () => {
|
|||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
const [editingPrompt, setEditingPrompt] = useState<Prompt | null>(null);
|
||||
|
||||
// ── Table refetch wrapper (stable signature used by FormGeneratorTable) ──
|
||||
const _lastTableParams = useRef<any>(undefined);
|
||||
const _tableRefetch = useCallback(async (params?: any) => {
|
||||
if (params) _lastTableParams.current = params;
|
||||
else params = _lastTableParams.current;
|
||||
await refetch(params);
|
||||
}, [refetch]);
|
||||
|
||||
// ── Refresh-All for the header "Aktualisieren" button ────────────────────
|
||||
// Forces a paginated request so the cache key matches what the table uses
|
||||
// internally. This guarantees fresh (non-cached) data is pulled in.
|
||||
const _refreshAll = useCallback(async () => {
|
||||
await _tableRefetch({ page: 1, pageSize: 25 });
|
||||
await _tableRefetch();
|
||||
}, [_tableRefetch]);
|
||||
|
||||
// Initial fetch
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Copyright (c) 2026 PowerOn AG
|
||||
// All rights reserved.
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react';
|
||||
import { FormGeneratorTable, type ColumnConfig } from '../../components/FormGenerator/FormGeneratorTable';
|
||||
import { StackLayout } from '../../components/Layout/StackLayout';
|
||||
import { Panel } from '../../components/Layout/Panel';
|
||||
|
|
@ -32,6 +32,12 @@ const AdminSubscriptionsPage: React.FC = () => {
|
|||
|
||||
const { confirm, ConfirmDialog } = useConfirm();
|
||||
const { data: subscriptions, pagination, loading, refetch } = useAdminSubscriptions();
|
||||
const _lastTableParams = useRef<any>(undefined);
|
||||
const _tableRefetch = useCallback(async (paginationParams?: any) => {
|
||||
if (paginationParams) _lastTableParams.current = paginationParams;
|
||||
else paginationParams = _lastTableParams.current;
|
||||
return refetch(paginationParams);
|
||||
}, [refetch]);
|
||||
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [dialogMode, setDialogMode] = useState<EnterpriseDialogMode>('create');
|
||||
|
|
@ -106,8 +112,8 @@ const AdminSubscriptionsPage: React.FC = () => {
|
|||
} else if (mode === 'update') {
|
||||
await updateEnterprise(request, values as any);
|
||||
}
|
||||
await refetch();
|
||||
}, [request, refetch]);
|
||||
await _tableRefetch();
|
||||
}, [request, _tableRefetch]);
|
||||
|
||||
const _rawColumns: ColumnConfig[] = useMemo(() => [
|
||||
{ key: 'mandateName', label: t('Mandant'), sortable: true, filterable: true, width: 180 },
|
||||
|
|
@ -153,11 +159,11 @@ const AdminSubscriptionsPage: React.FC = () => {
|
|||
|
||||
try {
|
||||
await api.post('/api/subscription/force-cancel', { subscriptionId: row.id });
|
||||
await refetch();
|
||||
await _tableRefetch();
|
||||
} catch (err) {
|
||||
console.error('Force cancel failed:', err);
|
||||
}
|
||||
}, [confirm, refetch, t]);
|
||||
}, [confirm, _tableRefetch, t]);
|
||||
|
||||
const _isEnterprise = (row: any) => row.isEnterprise || row.planKey === 'ENTERPRISE';
|
||||
|
||||
|
|
@ -215,7 +221,8 @@ const AdminSubscriptionsPage: React.FC = () => {
|
|||
pagination={true}
|
||||
pageSize={50}
|
||||
selectable={false}
|
||||
hookData={{ refetch, pagination }}
|
||||
initialSort={[{ key: 'startedAt', direction: 'desc' }]}
|
||||
hookData={{ refetch: _tableRefetch, pagination }}
|
||||
customActions={customActions}
|
||||
emptyMessage={t('Keine Abonnements vorhanden')}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -757,6 +757,7 @@ export const BillingDataView: React.FC = () => {
|
|||
filterable={true}
|
||||
sortable={true}
|
||||
selectable={false}
|
||||
initialSort={[{ key: 'sysCreatedAt', direction: 'desc' }]}
|
||||
emptyMessage={t('Keine Transaktionen vorhanden')}
|
||||
hookData={transactionsHookData}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
* 2. Gespraechspartner – Persona CRUD with FormGeneratorTable (apiEndpoint-driven)
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
||||
import { Link, useSearchParams } from 'react-router-dom';
|
||||
import { useApiRequest } from '../../../hooks/useApi';
|
||||
import { useInstanceId } from '../../../hooks/useCurrentInstance';
|
||||
|
|
@ -102,9 +102,12 @@ export const CommcoachSettingsView: React.FC = () => {
|
|||
const [personaError, setPersonaError] = useState<string | null>(null);
|
||||
|
||||
const personaApiEndpoint = instanceId ? `/api/commcoach/${instanceId}/personas` : undefined;
|
||||
const _lastPersonaParams = useRef<any>(undefined);
|
||||
|
||||
const _refetchPersonas = useCallback(async (params?: any) => {
|
||||
if (!instanceId) return;
|
||||
if (params) _lastPersonaParams.current = params;
|
||||
else params = _lastPersonaParams.current || { page: 1, pageSize: 50 };
|
||||
setPersonasLoading(true);
|
||||
try {
|
||||
const data = await fetchPersonasPaginated(request, instanceId, params);
|
||||
|
|
@ -125,7 +128,7 @@ export const CommcoachSettingsView: React.FC = () => {
|
|||
|
||||
useEffect(() => {
|
||||
if (activeTab === 'personas' && instanceId) {
|
||||
_refetchPersonas({ page: 1, pageSize: 50 });
|
||||
_refetchPersonas();
|
||||
}
|
||||
}, [activeTab, instanceId, _refetchPersonas]);
|
||||
|
||||
|
|
@ -159,7 +162,7 @@ export const CommcoachSettingsView: React.FC = () => {
|
|||
});
|
||||
setPersonaForm({ label: '', description: '', gender: '' });
|
||||
setShowCreatePersona(false);
|
||||
await _refetchPersonas({ page: 1, pageSize: 50 });
|
||||
await _refetchPersonas();
|
||||
} catch (err: any) {
|
||||
setPersonaError(err.message || 'Fehler beim Erstellen');
|
||||
} finally {
|
||||
|
|
@ -179,7 +182,7 @@ export const CommcoachSettingsView: React.FC = () => {
|
|||
});
|
||||
setEditingPersona(null);
|
||||
setPersonaForm({ label: '', description: '', gender: '' });
|
||||
await _refetchPersonas({ page: 1, pageSize: 50 });
|
||||
await _refetchPersonas();
|
||||
} catch (err: any) {
|
||||
setPersonaError(err.message || 'Fehler beim Speichern');
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
* Embedded — NO own StackLayout. Regions: Panel `toolbar` + Panel `table`.
|
||||
*/
|
||||
|
||||
import React, { useState, useMemo, useEffect } from 'react';
|
||||
import React, { useState, useMemo, useEffect, useCallback, useRef } from 'react';
|
||||
import { useTrusteeDocuments, useTrusteeDocumentOperations, TrusteeDocument } from '../../../hooks/useTrustee';
|
||||
import { useInstanceId } from '../../../hooks/useCurrentInstance';
|
||||
import { FormGeneratorTable } from '../../../components/FormGenerator/FormGeneratorTable';
|
||||
|
|
@ -58,12 +58,20 @@ export const TrusteeDocumentsView: React.FC = () => {
|
|||
const [editingDocument, setEditingDocument] = useState<TrusteeDocument | null>(null);
|
||||
const [isCreateMode, setIsCreateMode] = useState(false);
|
||||
const [downloadingId, setDownloadingId] = useState<string | null>(null);
|
||||
const _lastTableParams = useRef<any>(undefined);
|
||||
|
||||
const _tableRefetch = useCallback(async (paginationParams?: any) => {
|
||||
if (paginationParams) _lastTableParams.current = paginationParams;
|
||||
else paginationParams = _lastTableParams.current;
|
||||
return refetch(paginationParams);
|
||||
}, [refetch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (instanceId) {
|
||||
refetch();
|
||||
_lastTableParams.current = undefined;
|
||||
_tableRefetch();
|
||||
}
|
||||
}, [instanceId, refetch]);
|
||||
}, [instanceId, _tableRefetch]);
|
||||
|
||||
const documentColumnOrder = ['sysCreatedAt'];
|
||||
|
||||
|
|
@ -113,13 +121,13 @@ export const TrusteeDocumentsView: React.FC = () => {
|
|||
const result = await handleCreate(data);
|
||||
if (result.success) {
|
||||
setIsCreateMode(false);
|
||||
refetch();
|
||||
_tableRefetch();
|
||||
}
|
||||
} else if (editingDocument) {
|
||||
const result = await handleUpdate(editingDocument.id, data);
|
||||
if (result.success) {
|
||||
setEditingDocument(null);
|
||||
refetch();
|
||||
_tableRefetch();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -128,7 +136,7 @@ export const TrusteeDocumentsView: React.FC = () => {
|
|||
removeOptimistically(doc.id);
|
||||
const success = await handleDelete(doc.id);
|
||||
if (!success) {
|
||||
refetch();
|
||||
_tableRefetch();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -173,7 +181,7 @@ export const TrusteeDocumentsView: React.FC = () => {
|
|||
updateOptimistically(itemId, updateData);
|
||||
const result = await handleUpdate(itemId, { ...row, ...updateData });
|
||||
if (!result.success) {
|
||||
refetch();
|
||||
_tableRefetch();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -189,7 +197,7 @@ export const TrusteeDocumentsView: React.FC = () => {
|
|||
<p className={styles.errorMessage}>
|
||||
{t('Fehler beim Laden der Dokumente: {detail}', { detail: String(error) })}
|
||||
</p>
|
||||
<button type="button" className={styles.secondaryButton} onClick={() => refetch()}>
|
||||
<button type="button" className={styles.secondaryButton} onClick={() => _tableRefetch()}>
|
||||
<FaSync /> {t('Erneut versuchen')}
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -204,7 +212,7 @@ export const TrusteeDocumentsView: React.FC = () => {
|
|||
<button
|
||||
type="button"
|
||||
className={styles.secondaryButton}
|
||||
onClick={() => refetch()}
|
||||
onClick={() => _tableRefetch()}
|
||||
disabled={loading}
|
||||
>
|
||||
<FaSync className={loading ? 'spinning' : ''} /> {t('Aktualisieren')}
|
||||
|
|
@ -258,7 +266,7 @@ export const TrusteeDocumentsView: React.FC = () => {
|
|||
]}
|
||||
onDelete={handleDeleteDoc}
|
||||
hookData={{
|
||||
refetch,
|
||||
refetch: _tableRefetch,
|
||||
permissions,
|
||||
pagination,
|
||||
handleDelete,
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
* Embedded — NO own StackLayout. Regions: Panel `toolbar` + Panel `table`.
|
||||
*/
|
||||
|
||||
import React, { useState, useMemo, useEffect, useCallback } from 'react';
|
||||
import React, { useState, useMemo, useEffect, useCallback, useRef } from 'react';
|
||||
import { useTrusteePositions, useTrusteePositionOperations, TrusteePosition } from '../../../hooks/useTrustee';
|
||||
import { useInstanceId } from '../../../hooks/useCurrentInstance';
|
||||
import { useApiRequest } from '../../../hooks/useApi';
|
||||
|
|
@ -63,12 +63,20 @@ export const TrusteePositionsView: React.FC = () => {
|
|||
|
||||
const [editingPosition, setEditingPosition] = useState<TrusteePosition | null>(null);
|
||||
const [isCreateMode, setIsCreateMode] = useState(false);
|
||||
const _lastTableParams = useRef<any>(undefined);
|
||||
|
||||
const _tableRefetch = useCallback(async (paginationParams?: any) => {
|
||||
if (paginationParams) _lastTableParams.current = paginationParams;
|
||||
else paginationParams = _lastTableParams.current;
|
||||
return refetch(paginationParams);
|
||||
}, [refetch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (instanceId) {
|
||||
refetch();
|
||||
_lastTableParams.current = undefined;
|
||||
_tableRefetch();
|
||||
}
|
||||
}, [instanceId, refetch]);
|
||||
}, [instanceId, _tableRefetch]);
|
||||
|
||||
const handleBatchSyncToAccounting = useCallback(
|
||||
async (rows: TrusteePosition[]) => {
|
||||
|
|
@ -91,7 +99,7 @@ export const TrusteePositionsView: React.FC = () => {
|
|||
firstError?.errorMessage || t('{count} Fehler.', { count: res.errors }),
|
||||
);
|
||||
}
|
||||
refetch();
|
||||
_tableRefetch();
|
||||
} catch (err: any) {
|
||||
showError(
|
||||
t('Sync fehlgeschlagen'),
|
||||
|
|
@ -105,7 +113,7 @@ export const TrusteePositionsView: React.FC = () => {
|
|||
});
|
||||
}
|
||||
},
|
||||
[instanceId, request, refetch, showSuccess, showError, t],
|
||||
[instanceId, request, _tableRefetch, showSuccess, showError, t],
|
||||
);
|
||||
|
||||
const handleSingleSyncToAccounting = useCallback(
|
||||
|
|
@ -290,13 +298,13 @@ export const TrusteePositionsView: React.FC = () => {
|
|||
const result = await handleCreate(processedData);
|
||||
if (result.success) {
|
||||
setIsCreateMode(false);
|
||||
refetch();
|
||||
_tableRefetch();
|
||||
}
|
||||
} else if (editingPosition) {
|
||||
const result = await handleUpdate(editingPosition.id, processedData);
|
||||
if (result.success) {
|
||||
setEditingPosition(null);
|
||||
refetch();
|
||||
_tableRefetch();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -305,7 +313,7 @@ export const TrusteePositionsView: React.FC = () => {
|
|||
removeOptimistically(pos.id);
|
||||
const success = await handleDelete(pos.id);
|
||||
if (!success) {
|
||||
refetch();
|
||||
_tableRefetch();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -323,7 +331,7 @@ export const TrusteePositionsView: React.FC = () => {
|
|||
updateOptimistically(itemId, updateData);
|
||||
const result = await handleUpdate(itemId, { ...row, ...updateData });
|
||||
if (!result.success) {
|
||||
refetch();
|
||||
_tableRefetch();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -339,7 +347,7 @@ export const TrusteePositionsView: React.FC = () => {
|
|||
<p className={styles.errorMessage}>
|
||||
{t('Fehler beim Laden der Positionen: {detail}', { detail: String(error) })}
|
||||
</p>
|
||||
<button type="button" className={styles.secondaryButton} onClick={() => refetch()}>
|
||||
<button type="button" className={styles.secondaryButton} onClick={() => _tableRefetch()}>
|
||||
<FaSync /> {t('Erneut versuchen')}
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -354,7 +362,7 @@ export const TrusteePositionsView: React.FC = () => {
|
|||
<button
|
||||
type="button"
|
||||
className={styles.secondaryButton}
|
||||
onClick={() => refetch()}
|
||||
onClick={() => _tableRefetch()}
|
||||
disabled={loading}
|
||||
>
|
||||
<FaSync className={loading ? 'spinning' : ''} /> {t('Aktualisieren')}
|
||||
|
|
@ -416,7 +424,7 @@ export const TrusteePositionsView: React.FC = () => {
|
|||
]}
|
||||
onDelete={handleDeletePos}
|
||||
hookData={{
|
||||
refetch,
|
||||
refetch: _tableRefetch,
|
||||
permissions,
|
||||
pagination,
|
||||
handleDelete,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
* Embedded in WorkflowAutomationHubPage via TemplatesTab (embedded=true).
|
||||
*/
|
||||
|
||||
import React, { useState, useCallback, useEffect, useMemo } from 'react';
|
||||
import React, { useState, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { FaCopy, FaSync, FaShareAlt, FaPen } from 'react-icons/fa';
|
||||
import { usePrompt } from '../../../hooks/usePrompt';
|
||||
|
|
@ -89,6 +89,7 @@ export const WorkflowTemplatesPage: React.FC<WorkflowTemplatesPageProps> = ({
|
|||
|
||||
const [paginationMeta, setPaginationMeta] = useState<any>(null);
|
||||
const [backendAttributes, setBackendAttributes] = useState<AttributeDefinition[]>([]);
|
||||
const _lastTableParams = useRef<any>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
fetchAttributes(request, 'AutoWorkflowView')
|
||||
|
|
@ -100,6 +101,8 @@ export const WorkflowTemplatesPage: React.FC<WorkflowTemplatesPageProps> = ({
|
|||
|
||||
const load = useCallback(async (paginationParams?: any) => {
|
||||
if (!instanceId) return;
|
||||
if (paginationParams) _lastTableParams.current = paginationParams;
|
||||
else paginationParams = _lastTableParams.current;
|
||||
setLoading(true);
|
||||
try {
|
||||
const scope = activeScope === 'all' ? undefined : activeScope;
|
||||
|
|
|
|||
|
|
@ -278,9 +278,8 @@ export const WorkspaceInput = forwardRef<WorkspaceInputHandle, WorkspaceInputPro
|
|||
useEffect(() => {
|
||||
if (_sttPrefsLoaded.current) return;
|
||||
_sttPrefsLoaded.current = true;
|
||||
fetch('/api/voice/preferences', { credentials: 'include' })
|
||||
.then(r => r.ok ? r.json() : null)
|
||||
.then(data => { if (data?.sttLanguage) setVoiceLanguage(data.sttLanguage); })
|
||||
api.get('/api/voice/preferences')
|
||||
.then(res => { if (res.data?.sttLanguage) setVoiceLanguage(res.data.sttLanguage); })
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
|
|
@ -872,7 +871,7 @@ export const WorkspaceInput = forwardRef<WorkspaceInputHandle, WorkspaceInputPro
|
|||
onClick={() => {
|
||||
setVoiceLanguage(lang.bcp47);
|
||||
setShowLangPicker(false);
|
||||
fetch('/api/voice/preferences', { method: 'PUT', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sttLanguage: lang.bcp47 }) }).catch(() => {});
|
||||
api.put('/api/voice/preferences', { sttLanguage: lang.bcp47 }).catch(() => {});
|
||||
}}
|
||||
style={{
|
||||
padding: '8px 12px', cursor: 'pointer', fontSize: 13,
|
||||
|
|
|
|||
Loading…
Reference in a new issue