pushing to int
This commit is contained in:
parent
6988984cd7
commit
9519fed195
21 changed files with 883 additions and 30 deletions
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export { default as CopyActionButton } from './CopyActionButton';
|
||||
export type { CopyActionButtonProps } from './CopyActionButton';
|
||||
|
||||
|
|
@ -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');
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
140
src/components/ui/Button/CreateButton/CreateButton.tsx
Normal file
140
src/components/ui/Button/CreateButton/CreateButton.tsx
Normal 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;
|
||||
|
||||
3
src/components/ui/Button/CreateButton/index.ts
Normal file
3
src/components/ui/Button/CreateButton/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export { default as CreateButton } from './CreateButton';
|
||||
export { default } from './CreateButton';
|
||||
|
||||
|
|
@ -1,2 +1,4 @@
|
|||
export { default as Button } from './Button';
|
||||
export { UploadButton } from './UploadButton';
|
||||
export { CreateButton } from './CreateButton';
|
||||
export * from './ButtonTypes';
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
||||
];
|
||||
|
||||
|
|
|
|||
247
src/core/PageManager/data/pages/prompts.ts
Normal file
247
src/core/PageManager/data/pages/prompts.ts
Normal 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');
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
Loading…
Reference in a new issue