pushing to int

This commit is contained in:
Ida Dittrich 2025-10-12 16:16:21 +02:00
parent 6988984cd7
commit 9519fed195
21 changed files with 883 additions and 30 deletions

View file

@ -161,6 +161,15 @@
background: var(--color-secondary-hover);
}
.actionButton.copy {
background: var(--color-secondary);
color: white;
}
.actionButton.copy:hover {
background: var(--color-secondary-hover);
}
/* Responsive Design */
@media (max-width: 768px) {
.actionButtons {
@ -215,4 +224,12 @@
.actionButton.view:hover {
background: var(--color-secondary-hover);
}
.actionButton.copy {
background: var(--color-secondary);
}
.actionButton.copy:hover {
background: var(--color-secondary-hover);
}
}

View file

@ -0,0 +1,112 @@
import React, { useState } from 'react';
import { IoCopy } from 'react-icons/io5';
import { useLanguage } from '../../../../contexts/LanguageContext';
import styles from '../ActionButton.module.css';
export interface CopyActionButtonProps<T = any> {
row: T;
onCopy?: (row: T) => Promise<void> | void;
disabled?: boolean | { disabled: boolean; message?: string };
loading?: boolean;
className?: string;
title?: string;
isCopying?: boolean;
hookData?: any; // Contains all hook data including operations
// Field mappings
idField?: string; // Field name for the unique identifier
nameField?: string; // Field name for display name
contentField?: string; // Field name for content
loadingStateName?: string; // Name of the loading state in hookData
operationName?: string; // Name of the operation function in hookData
}
export function CopyActionButton<T = any>({
row,
onCopy,
disabled = false,
loading = false,
className = '',
title,
isCopying = false,
hookData,
idField = 'id',
nameField = 'name',
contentField = 'content',
loadingStateName = 'creatingPrompt',
operationName = 'handlePromptCreate'
}: CopyActionButtonProps<T>) {
const { t } = useLanguage();
const [internalLoading, setInternalLoading] = useState(false);
const [showCopiedFeedback, setShowCopiedFeedback] = useState(false);
// Extract disabled state and tooltip message
const isDisabled = typeof disabled === 'boolean' ? disabled : disabled?.disabled || false;
const disabledMessage = typeof disabled === 'object' ? disabled?.message : undefined;
const handleClick = async (e: React.MouseEvent) => {
e.stopPropagation();
if (!isDisabled && !loading && !isCopying && !internalLoading) {
setInternalLoading(true);
try {
// If operationName is provided and hookData is available, use the hook function
if (operationName && hookData && hookData[operationName]) {
// Extract data from row for creating a copy
const copyData = {
name: `${(row as any)[nameField]} (Copy)`,
content: (row as any)[contentField],
mandateId: (row as any).mandateId
};
const result = await hookData[operationName](copyData);
if (result.success) {
// Show copied feedback
setShowCopiedFeedback(true);
setTimeout(() => setShowCopiedFeedback(false), 2000);
// Refetch to update the list
if (hookData.refetch) {
await hookData.refetch();
}
}
} else if (onCopy) {
// Fallback to the provided onCopy function
await onCopy(row);
setShowCopiedFeedback(true);
setTimeout(() => setShowCopiedFeedback(false), 2000);
} else {
console.error('No copy function available');
}
} catch (error) {
console.error('Copy failed:', error);
} finally {
setInternalLoading(false);
}
}
};
const buttonTitle = title || t('prompts.action.copy', 'Copy');
// Use hookData copying state if available, otherwise use passed isCopying
const loadingState = hookData?.[loadingStateName];
const actualIsCopying = (typeof loadingState === 'boolean' && loadingState) || isCopying;
const isLoading = loading || actualIsCopying || internalLoading;
// Determine the final button title (tooltip)
const finalTitle = isDisabled && disabledMessage ? disabledMessage : buttonTitle;
return (
<button
onClick={handleClick}
className={`${styles.actionButton} ${styles.copy} ${isLoading ? styles.loading : ''} ${isDisabled ? styles.disabled : ''} ${className}`}
title={finalTitle}
disabled={isDisabled || isLoading}
>
<span className={styles.actionIcon}>
{showCopiedFeedback ? '✓' : isLoading ? '⏳' : <IoCopy />}
</span>
</button>
);
}
export default CopyActionButton;

View file

@ -0,0 +1,3 @@
export { default as CopyActionButton } from './CopyActionButton';
export type { CopyActionButtonProps } from './CopyActionButton';

View file

@ -35,7 +35,7 @@ export function DeleteActionButton<T = any>({
hookData,
idField = 'id',
operationName = 'handleDelete',
loadingStateName = 'deletingFiles'
loadingStateName = 'deletingItems'
}: DeleteActionButtonProps<T>) {
const { t } = useLanguage();
const [isConfirming, setIsConfirming] = useState(false);
@ -52,7 +52,7 @@ export function DeleteActionButton<T = any>({
// Extract operations from hookData
const handleDelete = hookData[operationName];
const removeOptimistically = hookData.removeFileOptimistically || hookData.removeOptimistically;
const removeOptimistically = hookData.removeOptimistically || hookData.removeFileOptimistically;
const refetch = hookData.refetch;
const loadingState = hookData[loadingStateName];
@ -117,18 +117,26 @@ export function DeleteActionButton<T = any>({
const success = await handleDelete(itemId);
if (success) {
// Refetch in background to sync with backend (non-blocking)
refetch(); // Non-blocking - let it run in background
// If we used optimistic removal, delay refetch to ensure server has synced
if (removeOptimistically) {
// Delay refetch by 500ms to give server time to fully process deletion
setTimeout(() => {
refetch();
}, 500);
} else {
// No optimistic removal, refetch immediately
refetch();
}
onSuccess?.(row);
} else {
// Refetch to restore the file in case of failure
// Refetch to restore the item in case of failure
await refetch();
onError?.(row, 'Delete failed');
}
} catch (error: any) {
console.error('Delete failed:', error);
onError?.(row, error.message || 'Delete failed');
// Refetch to restore the file in case of failure
// Refetch to restore the item in case of failure
await refetch();
} finally {
setIsDeleting(false);
@ -140,7 +148,7 @@ export function DeleteActionButton<T = any>({
setIsConfirming(false);
};
const buttonTitle = title || t('files.action.delete', 'Delete');
const buttonTitle = title || t('common.delete', 'Delete');
const confirmButtonTitle = confirmTitle || t('formgen.delete.confirm', 'Confirm delete');
const cancelButtonTitle = cancelTitle || t('formgen.delete.cancel', 'Cancel delete');

View file

@ -3,9 +3,11 @@ export { EditActionButton } from './EditActionButton';
export { DeleteActionButton } from './DeleteActionButton';
export { DownloadActionButton } from './DownloadActionButton';
export { ViewActionButton } from './ViewActionButton';
export { CopyActionButton } from './CopyActionButton';
// Action Button Types
export type { EditActionButtonProps } from './EditActionButton';
export type { DeleteActionButtonProps } from './DeleteActionButton';
export type { DownloadActionButtonProps } from './DownloadActionButton';
export type { ViewActionButtonProps } from './ViewActionButton';
export type { CopyActionButtonProps } from './CopyActionButton';

View file

@ -5,7 +5,8 @@ import {
EditActionButton,
DeleteActionButton,
DownloadActionButton,
ViewActionButton
ViewActionButton,
CopyActionButton
} from './ActionButtons';
import { Button } from '../ui/Button';
@ -46,7 +47,7 @@ export interface FormGeneratorProps<T = any> {
isRowSelectable?: (row: T) => boolean;
loading?: boolean;
actionButtons?: {
type: 'edit' | 'delete' | 'download' | 'view';
type: 'edit' | 'delete' | 'download' | 'view' | 'copy';
onAction?: (row: T) => Promise<void> | void; // Optional for delete buttons since they handle their own logic
disabled?: (row: T) => boolean | { disabled: boolean; message?: string };
loading?: (row: T) => boolean;
@ -58,6 +59,7 @@ export interface FormGeneratorProps<T = any> {
idField?: string; // Field name for the unique identifier
nameField?: string; // Field name for display name
typeField?: string; // Field name for type/mime type
contentField?: string; // Field name for content (used by copy button)
// Operation and loading state names
operationName?: string; // Name of the operation function in hookData
loadingStateName?: string; // Name of the loading state in hookData
@ -784,6 +786,7 @@ export function FormGenerator<T extends Record<string, any>>({
idField: actionButton.idField ?? 'id',
nameField: actionButton.nameField ?? 'name',
typeField: actionButton.typeField ?? 'type',
contentField: actionButton.contentField ?? 'content',
operationName: actionButton.operationName,
loadingStateName: actionButton.loadingStateName
};
@ -803,6 +806,8 @@ export function FormGenerator<T extends Record<string, any>>({
return <DownloadActionButton key={actionIndex} {...baseProps} onDownload={actionButton.onAction || (() => {})} isDownloading={isProcessing} hookData={hookData} operationName={actionButton.operationName} />;
case 'view':
return <ViewActionButton key={actionIndex} {...baseProps} onView={actionButton.onAction || (() => {})} isViewing={isProcessing} hookData={hookData} />;
case 'copy':
return <CopyActionButton key={actionIndex} {...baseProps} onCopy={actionButton.onAction} isCopying={isProcessing} hookData={hookData} operationName={actionButton.operationName} contentField={actionButton.contentField} />;
default:
return null;
}

View file

@ -26,3 +26,26 @@ export interface UploadButtonProps extends BaseButtonProps {
icon?: IconType;
iconPosition?: 'left' | 'right';
}
export interface CreateButtonFieldConfig {
key: string;
label: string;
type: 'string' | 'boolean' | 'email' | 'textarea' | 'date' | 'enum' | 'readonly';
required?: boolean;
placeholder?: string;
minRows?: number;
maxRows?: number;
validator?: (value: any) => string | null;
defaultValue?: any;
}
export interface CreateButtonProps extends BaseButtonProps {
onCreate: (data: any) => Promise<any>;
fields: CreateButtonFieldConfig[];
popupTitle?: string;
popupSize?: 'small' | 'medium' | 'large';
icon?: IconType;
iconPosition?: 'left' | 'right';
onSuccess?: (result: any) => void;
onError?: (error: string) => void;
}

View file

@ -0,0 +1,140 @@
import React, { useState } from 'react';
import { CreateButtonProps } from '../ButtonTypes';
import Button from '../Button';
import { Popup, EditForm } from '../../Popup';
import { useLanguage } from '../../../../contexts/LanguageContext';
const CreateButton: React.FC<CreateButtonProps> = ({
onCreate,
fields,
popupTitle = 'Create New Item',
popupSize = 'medium',
disabled = false,
loading = false,
className = '',
children,
icon,
iconPosition = 'left',
variant = 'primary',
size = 'md',
onSuccess,
onError,
...props
}) => {
const { t } = useLanguage();
const [isCreating, setIsCreating] = useState(false);
const [isPopupOpen, setIsPopupOpen] = useState(false);
const [formData, setFormData] = useState<any>({});
// Initialize form data with default values
React.useEffect(() => {
const initialData: any = {};
fields.forEach(field => {
initialData[field.key] = field.defaultValue || '';
});
setFormData(initialData);
}, [fields]);
const handleButtonClick = () => {
if (!disabled && !loading && !isCreating) {
// Reset form data
const initialData: any = {};
fields.forEach(field => {
initialData[field.key] = field.defaultValue || '';
});
setFormData(initialData);
setIsPopupOpen(true);
}
};
const handleSave = async (updatedData: any) => {
setIsCreating(true);
try {
const result = await onCreate(updatedData);
if (result?.success !== false) {
// Success
setIsPopupOpen(false);
if (onSuccess) {
onSuccess(result);
}
} else {
// Handle error
if (onError) {
onError(result?.error || 'Creation failed');
}
}
} catch (error: any) {
console.error('Creation failed:', error);
if (onError) {
onError(error.message || 'Creation failed');
}
} finally {
setIsCreating(false);
}
};
const handleCancel = () => {
setIsPopupOpen(false);
};
const isDisabled = disabled || loading || isCreating;
// Resolve language text for popup title
const resolvedPopupTitle = typeof popupTitle === 'string'
? t(popupTitle, popupTitle)
: popupTitle;
// Resolve language text for fields
const resolvedFields = fields.map(field => ({
...field,
label: typeof field.label === 'string' ? t(field.label, field.label) : field.label,
placeholder: field.placeholder
? (typeof field.placeholder === 'string' ? t(field.placeholder, field.placeholder) : field.placeholder)
: undefined,
editable: true
}));
return (
<>
<Button
{...props}
variant={variant}
size={size}
disabled={isDisabled}
loading={false}
className={`createButton ${className}`}
onClick={handleButtonClick}
icon={isCreating ? undefined : icon}
iconPosition={iconPosition}
>
{isCreating && (
<div className="spinnerIcon" style={{ marginRight: '8px' }} />
)}
{children || (isCreating ? t('common.creating', 'Creating...') : t('common.create', 'Create'))}
</Button>
{/* Create Popup */}
<Popup
isOpen={isPopupOpen}
title={resolvedPopupTitle}
onClose={handleCancel}
size={popupSize}
closable={!isCreating}
>
<EditForm
data={formData}
fields={resolvedFields}
onSave={handleSave}
onCancel={handleCancel}
saveButtonText={t('common.create', 'Create')}
cancelButtonText={t('common.cancel', 'Cancel')}
/>
</Popup>
</>
);
};
export default CreateButton;

View file

@ -0,0 +1,3 @@
export { default as CreateButton } from './CreateButton';
export { default } from './CreateButton';

View file

@ -1,2 +1,4 @@
export { default as Button } from './Button';
export { UploadButton } from './UploadButton';
export { CreateButton } from './CreateButton';
export * from './ButtonTypes';

View file

@ -1,7 +1,7 @@
import React from 'react';
import { GenericPageData, PageButton, PageContent, resolveLanguageText } from './pageInterface';
import { FormGenerator } from '../../components/FormGenerator';
import { Button, UploadButton } from '../../components/ui';
import { Button, UploadButton, CreateButton } from '../../components/ui';
import { DragDropOverlay } from '../../components/ui/DragDropOverlay';
import { useLanguage } from '../../contexts/LanguageContext';
import styles from '../../styles/pages.module.css';
@ -253,11 +253,9 @@ const PageRenderer: React.FC<PageRendererProps> = ({
{pageData.headerButtons && pageData.headerButtons.length > 0 && (
<div className={styles.headerButtons}>
{pageData.headerButtons.map((button) => {
// Check if this is an upload button
if (button.id === 'upload-file') {
// Check if this is an upload button (has handleUpload in hookData)
const handleUpload = (hookData as any)?.handleUpload;
if (handleUpload) {
if (handleUpload && button.id === 'upload-file') {
return (
<UploadButton
key={button.id}
@ -273,6 +271,43 @@ const PageRenderer: React.FC<PageRendererProps> = ({
</UploadButton>
);
}
// Check if this button has a formConfig (create button)
if (button.formConfig && hookData) {
const createOperation = button.formConfig.createOperationName
? (hookData as any)[button.formConfig.createOperationName]
: null;
if (createOperation) {
// Resolve field labels
const resolvedFields = button.formConfig.fields.map(field => ({
...field,
label: resolveLanguageText(field.label, t),
placeholder: field.placeholder ? resolveLanguageText(field.placeholder, t) : undefined
}));
return (
<CreateButton
key={button.id}
onCreate={createOperation}
fields={resolvedFields}
popupTitle={resolveLanguageText(button.formConfig.popupTitle || 'Create New Item', t)}
popupSize={button.formConfig.popupSize || 'medium'}
variant={button.variant || 'primary'}
size={button.size || 'md'}
icon={button.icon}
disabled={button.disabled}
onSuccess={() => {
// Refetch data after successful creation
if (hookData.refetch) {
hookData.refetch();
}
}}
>
{resolveLanguageText(button.label, t)}
</CreateButton>
);
}
}
// Regular button

View file

@ -212,7 +212,7 @@ export const filesPageData: GenericPageData = {
title: 'files.action.delete',
idField: 'id',
operationName: 'handleDelete',
loadingStateName: 'deletingFiles'
loadingStateName: 'deletingFiles' // Keep for backward compatibility
}
],
searchable: true,
@ -232,6 +232,7 @@ export const filesPageData: GenericPageData = {
// Page behavior
persistent: false,
preload: false,
preserveState: true, // Keep page mounted and prevent refetching
moduleEnabled: true,
// Sidebar - will be shown as subpage under Administration

View file

@ -3,12 +3,14 @@ export { dashboardPageData } from './dashboard';
export { filesPageData } from './files';
export { teamBereichPageData } from './team-bereich';
export { administrationPageData } from './administration';
export { promptsPageData } from './prompts';
// Import all page data
import { dashboardPageData } from './dashboard';
import { administrationPageData } from './administration';
import { filesPageData } from './files';
import { teamBereichPageData } from './team-bereich';
import { promptsPageData } from './prompts';
// Array of all page data
export const allPageData = [
@ -16,6 +18,7 @@ export const allPageData = [
administrationPageData,
filesPageData,
teamBereichPageData,
promptsPageData,
];

View file

@ -0,0 +1,247 @@
import { useCallback } from 'react';
import { GenericPageData } from '../../pageInterface';
import { FaLightbulb, FaPlus } from 'react-icons/fa';
import { privilegeCheckers } from '../../../../utils/privilegeCheckers';
import { usePrompts, usePromptOperations } from '../../../../hooks/usePrompts';
// Hook factory function for prompts data
const createPromptsHook = () => {
return () => {
const { prompts, loading, error, refetch, removeOptimistically } = usePrompts();
const {
handlePromptDelete,
handlePromptCreate,
handlePromptUpdate,
deletingPrompts,
creatingPrompt,
deleteError,
createError,
updateError
} = usePromptOperations();
// Wrapped create handler that adds mandateId automatically
const wrappedHandlePromptCreate = useCallback(async (formData: { name: string; content: string }) => {
// Get mandateId from the first prompt if available, or use empty string
const mandateId = prompts.length > 0 ? prompts[0].mandateId : '';
const promptData = {
name: formData.name,
content: formData.content,
mandateId: mandateId
};
return await handlePromptCreate(promptData);
}, [handlePromptCreate, prompts]);
// Handle single prompt deletion for FormGenerator
const handleDeleteSingle = useCallback(async (prompt: any) => {
const success = await handlePromptDelete(prompt.id);
if (success) {
refetch();
}
}, [handlePromptDelete, refetch]);
// Handle multiple prompt deletion for FormGenerator
const handleDeleteMultiple = useCallback(async (selectedPrompts: any[]) => {
const promptIds = selectedPrompts.map(prompt => prompt.id);
const results = await Promise.all(
promptIds.map(id => handlePromptDelete(id))
);
const allSuccessful = results.every(result => result);
if (allSuccessful) {
refetch();
}
}, [handlePromptDelete, refetch]);
return {
data: prompts,
loading,
error,
refetch,
removeOptimistically, // Expose optimistic removal (generic name for DeleteActionButton)
// Operations
handleDelete: handlePromptDelete,
handleDeleteMultiple,
handlePromptCreate: wrappedHandlePromptCreate, // Use wrapped version
handlePromptUpdate,
// FormGenerator specific handlers
onDelete: handleDeleteSingle,
onDeleteMultiple: handleDeleteMultiple,
// Loading states
deletingPrompts,
creatingPrompt,
// Error states
deleteError,
createError,
updateError
};
};
};
// Static columns configuration for prompts table
const promptsColumns = [
{
key: 'name',
label: 'prompts.column.name',
type: 'string',
width: 250,
minWidth: 150,
maxWidth: 350,
sortable: true,
filterable: true,
searchable: true
},
{
key: 'content',
label: 'prompts.column.content',
type: 'string',
width: 400,
minWidth: 200,
maxWidth: 600,
sortable: true,
filterable: true,
searchable: true
}
];
export const promptsPageData: GenericPageData = {
id: 'administration-prompts',
path: 'administration/prompts',
name: 'prompts.title',
description: 'prompts.description',
// Parent page
parentPath: 'administration',
// Visual
icon: FaLightbulb,
title: 'prompts.title',
subtitle: 'prompts.subtitle',
// Header buttons
headerButtons: [
{
id: 'new-prompt',
label: 'prompts.new_button',
icon: FaPlus,
variant: 'primary',
formConfig: {
fields: [
{
key: 'name',
label: 'prompts.field.name',
type: 'string',
required: true,
placeholder: 'prompts.field.name',
validator: (value: string) => {
if (!value || value.trim() === '') {
return 'Prompt name cannot be empty';
}
if (value.length > 100) {
return 'Prompt name cannot exceed 100 characters';
}
return null;
}
},
{
key: 'content',
label: 'prompts.field.content',
type: 'textarea',
required: true,
placeholder: 'prompts.field.content',
minRows: 6,
maxRows: 12,
validator: (value: string) => {
if (!value || value.trim() === '') {
return 'Prompt content cannot be empty';
}
if (value.length > 10000) {
return 'Prompt content cannot exceed 10,000 characters';
}
return null;
}
}
],
popupTitle: 'prompts.modal.create.title',
popupSize: 'medium',
createOperationName: 'handlePromptCreate',
successMessage: 'prompts.create.success',
errorMessage: 'prompts.create.error'
}
}
],
// Content sections - using generic table approach
content: [
{
id: 'prompts-table',
type: 'table',
tableConfig: {
hookFactory: createPromptsHook,
columns: promptsColumns,
actionButtons: [
{
type: 'edit',
title: 'prompts.action.edit',
idField: 'id',
nameField: 'name',
operationName: 'handlePromptUpdate',
loadingStateName: 'updatingPrompts'
},
{
type: 'delete',
title: 'prompts.action.delete',
idField: 'id',
operationName: 'handleDelete',
loadingStateName: 'deletingPrompts' // Entity-specific loading state
},
{
type: 'copy',
title: 'prompts.action.copy',
idField: 'id',
nameField: 'name',
contentField: 'content',
operationName: 'handlePromptCreate',
loadingStateName: 'creatingPrompt'
}
],
searchable: true,
filterable: true,
sortable: true,
resizable: true,
pagination: true,
pageSize: 10,
className: 'prompts-table'
}
}
],
// Privilege system
privilegeChecker: privilegeCheckers.viewerRole,
// Page behavior
persistent: false,
preload: false,
preserveState: true, // Keep page mounted and prevent refetching
moduleEnabled: true,
// shown in sidebar under administration
showInSidebar: false,
// No drag and drop configuration for prompts
// Lifecycle hooks
onActivate: async () => {
if (import.meta.env.DEV) console.log('Prompts activated');
},
onLoad: async () => {
if (import.meta.env.DEV) console.log('Prompts loaded - can initialize prompts list here');
},
onUnload: async () => {
if (import.meta.env.DEV) console.log('Prompts unloaded - cleanup prompts references');
}
};

View file

@ -5,6 +5,19 @@ import { DragDropConfig } from '../../components/ui/DragDropOverlay/DragDropOver
// Generic privilege checker function type
export type PrivilegeChecker = () => boolean | Promise<boolean>;
// Form field configuration for create/edit buttons
export interface ButtonFormField {
key: string;
label: string | LanguageText;
type: 'string' | 'boolean' | 'email' | 'textarea' | 'date' | 'enum' | 'readonly';
required?: boolean;
placeholder?: string | LanguageText;
minRows?: number;
maxRows?: number;
validator?: (value: any) => string | null;
defaultValue?: any;
}
// Button configuration for header actions
export interface PageButton {
id: string;
@ -15,6 +28,15 @@ export interface PageButton {
onClick?: (hookData?: any) => void | Promise<void>;
disabled?: boolean;
privilegeChecker?: PrivilegeChecker;
// Form configuration for create buttons
formConfig?: {
fields: ButtonFormField[];
popupTitle?: string | LanguageText;
popupSize?: 'small' | 'medium' | 'large';
createOperationName?: string; // Name of the create operation in hookData (e.g., 'handlePromptCreate')
successMessage?: string | LanguageText;
errorMessage?: string | LanguageText;
};
}
// Content section for paragraphs
@ -54,7 +76,7 @@ export interface GenericDataHook {
// Action button configuration
export interface ActionButtonConfig {
type: 'view' | 'edit' | 'download' | 'delete';
type: 'view' | 'edit' | 'download' | 'delete' | 'copy';
onAction?: (row: any) => Promise<void> | void; // Optional for delete buttons since they handle their own logic
title?: string | LanguageText;
disabled?: (row: any) => boolean | { disabled: boolean; message?: string };
@ -63,6 +85,7 @@ export interface ActionButtonConfig {
idField?: string; // Field name for the unique identifier (default: 'id')
nameField?: string; // Field name for display name (default: 'name' or 'file_name')
typeField?: string; // Field name for type/mime type (default: 'type' or 'mime_type')
contentField?: string; // Field name for content (default: 'content')
// Operation and loading state names
operationName?: string; // Name of the operation function in hookData
loadingStateName?: string; // Name of the loading state in hookData

View file

@ -935,13 +935,79 @@ export function useLogout() {
try {
// Call logout endpoint to clear JWT tokens on server
await api.post('/api/local/logout');
const logoutResponse = await api.post('/api/local/logout');
// Clear local storage (user data and auth_authority)
// Note: JWT tokens are now stored in httpOnly cookies and cleared by backend
console.log('✅ Logout API call completed, waiting for browser to process cookies...');
// CRITICAL: Wait for browser to process Set-Cookie headers from logout response
// This gives the browser time to clear httpOnly cookies before redirect
await new Promise(resolve => setTimeout(resolve, 1000));
// Clear all authentication-related localStorage items
localStorage.removeItem('currentUser');
localStorage.removeItem('auth_authority');
// Clear MSAL cache tokens from localStorage
// MSAL stores tokens with keys starting with 'msal.'
const keysToRemove = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && (
key.startsWith('msal.') ||
key === 'auth_token' ||
key === 'refresh_token' ||
key.includes('token') ||
key.includes('auth') ||
key.includes('msal')
)) {
keysToRemove.push(key);
}
}
keysToRemove.forEach(key => {
console.log('🗑️ Removing token:', key);
localStorage.removeItem(key);
});
// Clear debug items and other auth-related data
localStorage.removeItem('msft_auth_debug');
localStorage.removeItem('msft_cookie_debug');
// Clear ALL MSAL cache data (including account keys, token keys, version)
const msalKeysToRemove = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && key.startsWith('msal.')) {
msalKeysToRemove.push(key);
}
}
msalKeysToRemove.forEach(key => {
console.log('🗑️ Removing MSAL cache:', key);
localStorage.removeItem(key);
});
// Clear sessionStorage as well (CSRF tokens, etc.)
sessionStorage.clear();
// Clear cookies as backup (in case backend doesn't clear them properly)
// Note: This only works for cookies that are accessible to JavaScript
console.log('🍪 Checking cookies for cleanup...');
console.log('🍪 All cookies:', document.cookie);
const cookies = document.cookie.split(";");
console.log('🍪 Cookie count:', cookies.length);
cookies.forEach(function(c) {
const cookieName = c.split("=")[0].trim();
console.log('🍪 Checking cookie:', cookieName);
if (cookieName === 'auth_token' || cookieName === 'refresh_token' || cookieName.includes('token') || cookieName.includes('msal')) {
console.log('🗑️ Clearing cookie:', cookieName);
document.cookie = cookieName + "=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
}
});
console.log('🍪 Cookies after cleanup attempt:', document.cookie);
// Redirect to login page
window.location.href = '/login?logout=true';
} catch (error: any) {
@ -954,9 +1020,58 @@ export function useLogout() {
setError(errorMessage);
// Even if logout fails on server, clear local data and redirect
// Note: JWT tokens are now stored in httpOnly cookies and cleared by backend
localStorage.removeItem('currentUser');
localStorage.removeItem('auth_authority');
// Clear MSAL cache tokens from localStorage
const keysToRemove = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && (
key.startsWith('msal.') ||
key === 'auth_token' ||
key === 'refresh_token' ||
key.includes('token') ||
key.includes('auth') ||
key.includes('msal')
)) {
keysToRemove.push(key);
}
}
keysToRemove.forEach(key => {
console.log('🗑️ Removing token (error case):', key);
localStorage.removeItem(key);
});
// Clear debug items and other auth-related data
localStorage.removeItem('msft_auth_debug');
localStorage.removeItem('msft_cookie_debug');
// Clear ALL MSAL cache data (including account keys, token keys, version)
const msalKeysToRemove = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && key.startsWith('msal.')) {
msalKeysToRemove.push(key);
}
}
msalKeysToRemove.forEach(key => {
console.log('🗑️ Removing MSAL cache (error case):', key);
localStorage.removeItem(key);
});
// Clear sessionStorage as well
sessionStorage.clear();
// Clear cookies as backup (in case backend doesn't clear them properly)
document.cookie.split(";").forEach(function(c) {
const cookieName = c.split("=")[0].trim();
if (cookieName === 'auth_token' || cookieName === 'refresh_token' || cookieName.includes('token') || cookieName.includes('msal')) {
console.log('🗑️ Clearing cookie (error case):', cookieName);
document.cookie = cookieName + "=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
}
});
window.location.href = '/login?logout=true';
} finally {
setIsLoading(false);

View file

@ -32,11 +32,22 @@ export function usePrompts() {
}
};
// Optimistically remove a prompt from the local state
const removeOptimistically = (promptId: string) => {
setPrompts(prevPrompts => prevPrompts.filter(prompt => prompt.id !== promptId));
};
useEffect(() => {
fetchPrompts();
}, []);
return { prompts, loading, error, refetch: fetchPrompts };
return {
prompts,
loading,
error,
refetch: fetchPrompts,
removeOptimistically
};
}
// Prompt operations hook

View file

@ -151,22 +151,100 @@ export function useCurrentUser() {
logoutEndpoint = '/api/local/logout';
}
await request({
const logoutResponse = await request({
url: logoutEndpoint,
method: 'post'
});
console.log('✅ Logout API call completed, waiting for browser to process cookies...');
// CRITICAL: Wait for browser to process Set-Cookie headers from logout response
// This gives the browser time to clear httpOnly cookies before redirect
await new Promise(resolve => setTimeout(resolve, 1000));
// Clear user state after successful logout
setUser(null);
// Clear any local storage data
localStorage.clear();
// CRITICAL: Clear all authentication data BEFORE any redirects
// This ensures cleanup happens even if MSAL redirect interrupts the process
console.log('🧹 Starting comprehensive localStorage cleanup...');
// Clear all authentication-related localStorage items
localStorage.removeItem('currentUser');
localStorage.removeItem('auth_authority');
// Clear MSAL cache tokens from localStorage
// MSAL stores tokens with keys starting with 'msal.'
const keysToRemove = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && (
key.startsWith('msal.') ||
key === 'auth_token' ||
key === 'refresh_token' ||
key.includes('token') ||
key.includes('auth') ||
key.includes('msal')
)) {
keysToRemove.push(key);
}
}
keysToRemove.forEach(key => {
console.log('🗑️ Removing token:', key);
localStorage.removeItem(key);
});
// Clear debug items and other auth-related data
localStorage.removeItem('msft_auth_debug');
localStorage.removeItem('msft_cookie_debug');
// Clear ALL MSAL cache data (including account keys, token keys, version)
const msalKeysToRemove = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && key.startsWith('msal.')) {
msalKeysToRemove.push(key);
}
}
msalKeysToRemove.forEach(key => {
console.log('🗑️ Removing MSAL cache:', key);
localStorage.removeItem(key);
});
// Clear sessionStorage as well (CSRF tokens, etc.)
sessionStorage.clear();
// Clear cookies as backup (in case backend doesn't clear them properly)
// Note: This only works for cookies that are accessible to JavaScript
console.log('🍪 Checking cookies for cleanup...');
console.log('🍪 All cookies:', document.cookie);
const cookies = document.cookie.split(";");
console.log('🍪 Cookie count:', cookies.length);
cookies.forEach(function(c) {
const cookieName = c.split("=")[0].trim();
console.log('🍪 Checking cookie:', cookieName);
if (cookieName === 'auth_token' || cookieName === 'refresh_token' || cookieName.includes('token') || cookieName.includes('msal')) {
console.log('🗑️ Clearing cookie:', cookieName);
document.cookie = cookieName + "=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
}
});
console.log('🍪 Cookies after cleanup attempt:', document.cookie);
console.log('✅ localStorage cleanup completed');
// Handle MSAL logout for Microsoft authentication
if (user.authenticationAuthority === 'msft' && msalInstance) {
try {
console.log('🔄 Starting MSAL logout redirect...');
await msalInstance.logoutRedirect({
onRedirectNavigate: () => true
onRedirectNavigate: () => {
console.log('🔄 MSAL redirect initiated - cleanup already completed');
return true;
}
});
return; // MSAL will handle the redirect
} catch (msalError) {
@ -176,6 +254,7 @@ export function useCurrentUser() {
}
// Redirect to login or home page
console.log('🔄 Redirecting to login page...');
window.location.href = '/login';
} catch (error) {

View file

@ -60,6 +60,8 @@ export default {
'common.edit': 'Bearbeiten',
'common.close': 'Schließen',
'common.retry': 'Wiederholen',
'common.create': 'Erstellen',
'common.creating': 'Erstellen...',
// Auth
'auth.login': 'Anmelden',
@ -441,10 +443,14 @@ export default {
// Prompts
'prompts.title': 'Prompts',
'prompts.subtitle': 'Prompts verwalten',
'prompts.description': 'Prompts für Ihren KI-Assistenten erstellen und verwalten',
'prompts.new_button': 'Neuer Prompt',
'prompts.addNew': 'Prompt hinzufügen',
'prompts.creating': 'Erstellen...',
'prompts.column.name': 'Name',
'prompts.column.content': 'Inhalt',
'prompts.column.mandateId': 'Mandat-ID',
'prompts.unnamed': 'Unbenannt',
'prompts.action.edit': 'Bearbeiten',
'prompts.action.copy': 'Kopieren',
@ -463,6 +469,8 @@ export default {
'prompts.modal.edit.save': 'Änderungen speichern',
'prompts.modal.create.title': 'Neuen Prompt erstellen',
'prompts.modal.create.save': 'Prompt erstellen',
'prompts.create.success': 'Prompt erfolgreich erstellt',
'prompts.create.error': 'Fehler beim Erstellen des Prompts',
// Users/Members
'users.title': 'Benutzer',

View file

@ -60,6 +60,8 @@ export default {
'common.edit': 'Edit',
'common.close': 'Close',
'common.retry': 'Retry',
'common.create': 'Create',
'common.creating': 'Creating...',
// Auth
'auth.login': 'Login',
@ -441,10 +443,14 @@ export default {
// Prompts
'prompts.title': 'Prompts',
'prompts.subtitle': 'Manage your prompts',
'prompts.description': 'Create and manage prompts for your AI assistant',
'prompts.new_button': 'New Prompt',
'prompts.addNew': 'Add Prompt',
'prompts.creating': 'Creating...',
'prompts.column.name': 'Name',
'prompts.column.content': 'Content',
'prompts.column.mandateId': 'Mandate ID',
'prompts.unnamed': 'Unnamed',
'prompts.action.edit': 'Edit',
'prompts.action.copy': 'Copy',
@ -463,6 +469,8 @@ export default {
'prompts.modal.edit.save': 'Save Changes',
'prompts.modal.create.title': 'Create New Prompt',
'prompts.modal.create.save': 'Create Prompt',
'prompts.create.success': 'Prompt created successfully',
'prompts.create.error': 'Error creating prompt',
// Users/Members
'users.title': 'Users',

View file

@ -60,6 +60,8 @@ export default {
'common.edit': 'Modifier',
'common.close': 'Fermer',
'common.retry': 'Réessayer',
'common.create': 'Créer',
'common.creating': 'Création...',
// Auth
'auth.login': 'Se connecter',
@ -441,10 +443,14 @@ export default {
// Prompts
'prompts.title': 'Prompts',
'prompts.subtitle': 'Gérer vos prompts',
'prompts.description': 'Créer et gérer des prompts pour votre assistant IA',
'prompts.new_button': 'Nouveau prompt',
'prompts.addNew': 'Ajouter un prompt',
'prompts.creating': 'Création...',
'prompts.column.name': 'Nom',
'prompts.column.content': 'Contenu',
'prompts.column.mandateId': 'ID Mandat',
'prompts.unnamed': 'Sans nom',
'prompts.action.edit': 'Modifier',
'prompts.action.copy': 'Copier',
@ -463,6 +469,8 @@ export default {
'prompts.modal.edit.save': 'Enregistrer les modifications',
'prompts.modal.create.title': 'Créer un nouveau prompt',
'prompts.modal.create.save': 'Créer le prompt',
'prompts.create.success': 'Prompt créé avec succès',
'prompts.create.error': 'Erreur lors de la création du prompt',
// Users/Members
'users.title': 'Utilisateurs',