This commit is contained in:
ValueOn AG 2026-01-13 21:17:27 +01:00
parent b2c38e75bf
commit 71666d2891
6 changed files with 72 additions and 31 deletions

View file

@ -193,29 +193,31 @@ export async function updateConnection(
/** /**
* Refresh Microsoft token * Refresh Microsoft token
* Endpoint: POST /api/connections/{connectionId}/refresh-microsoft-token * Endpoint: POST /api/msft/refresh
*/ */
export async function refreshMicrosoftToken( export async function refreshMicrosoftToken(
request: ApiRequestFunction, request: ApiRequestFunction,
connectionId: string connectionId: string
): Promise<Connection> { ): Promise<Connection> {
return await request({ return await request({
url: `/api/connections/${connectionId}/refresh-microsoft-token`, url: '/api/msft/refresh',
method: 'post' method: 'post',
data: { connectionId }
}); });
} }
/** /**
* Refresh Google token * Refresh Google token
* Endpoint: POST /api/connections/{connectionId}/refresh-google-token * Endpoint: POST /api/google/refresh
*/ */
export async function refreshGoogleToken( export async function refreshGoogleToken(
request: ApiRequestFunction, request: ApiRequestFunction,
connectionId: string connectionId: string
): Promise<Connection> { ): Promise<Connection> {
return await request({ return await request({
url: `/api/connections/${connectionId}/refresh-google-token`, url: '/api/google/refresh',
method: 'post' method: 'post',
data: { connectionId }
}); });
} }

View file

@ -1,4 +1,3 @@
import React from 'react';
import { useLanguage } from '../../../providers/language/LanguageContext'; import { useLanguage } from '../../../providers/language/LanguageContext';
import styles from './FormGeneratorControls.module.css'; import styles from './FormGeneratorControls.module.css';
import { Button } from '../../UiComponents/Button'; import { Button } from '../../UiComponents/Button';

View file

@ -1,6 +1,6 @@
import { useCallback } from 'react'; import React, { useCallback } from 'react';
import { GenericPageData } from '../../pageInterface'; import { GenericPageData } from '../../pageInterface';
import { FaGoogle, FaMicrosoft, FaLink } from 'react-icons/fa'; import { FaGoogle, FaMicrosoft, FaLink, FaSync, FaPlug } from 'react-icons/fa';
import { useConnections } from '../../../../hooks/useConnections'; import { useConnections } from '../../../../hooks/useConnections';
// Helper function to convert attribute definitions to column config // Helper function to convert attribute definitions to column config
@ -29,6 +29,8 @@ const createConnectionsHook = () => {
createGoogleConnectionAndAuth, createGoogleConnectionAndAuth,
createMicrosoftConnectionAndAuth, createMicrosoftConnectionAndAuth,
connectWithPopup, connectWithPopup,
refreshMicrosoftToken,
refreshGoogleToken,
isConnecting, isConnecting,
isLoading, isLoading,
error, error,
@ -92,9 +94,13 @@ const createConnectionsHook = () => {
connectWithPopup, connectWithPopup,
createGoogleConnectionAndAuth, createGoogleConnectionAndAuth,
createMicrosoftConnectionAndAuth, createMicrosoftConnectionAndAuth,
// Token refresh operations
refreshMicrosoftToken,
refreshGoogleToken,
// Loading states // Loading states
isConnecting, isConnecting,
deletingConnections: new Set(), // Placeholder for consistency with other pages deletingConnections: new Set(), // Placeholder for consistency with other pages
refreshingConnections: new Set<string>(), // Track which connections are refreshing
// Attributes and permissions for dynamic column/button generation // Attributes and permissions for dynamic column/button generation
attributes, attributes,
permissions, permissions,
@ -192,28 +198,14 @@ export const connectionsPageData: GenericPageData = {
tableConfig: { tableConfig: {
hookFactory: createConnectionsHook, hookFactory: createConnectionsHook,
// Columns are generated dynamically from attributes via hookData.columns // Columns are generated dynamically from attributes via hookData.columns
// Standard action buttons
actionButtons: [ actionButtons: [
{
type: 'connect',
title: 'connections.action.connect',
idField: 'id',
statusField: 'status',
operationName: 'connectWithPopup',
loadingStateName: 'isConnecting',
// Only show if user has update permission (connect = update operation)
disabled: (hookData: any) => {
if (!hookData?.permissions) return { disabled: false };
const hasUpdate = hookData.permissions.update !== 'n' && hookData.permissions.view;
return { disabled: !hasUpdate, message: 'No permission to connect' };
}
},
{ {
type: 'delete', type: 'delete',
title: 'connections.action.delete', title: 'connections.action.delete',
idField: 'id', idField: 'id',
operationName: 'handleDelete', operationName: 'handleDelete',
loadingStateName: 'deletingConnections', loadingStateName: 'deletingConnections',
// Only show if user has delete permission
disabled: (hookData: any) => { disabled: (hookData: any) => {
if (!hookData?.permissions) return { disabled: false }; if (!hookData?.permissions) return { disabled: false };
const hasDelete = hookData.permissions.delete !== 'n' && hookData.permissions.view; const hasDelete = hookData.permissions.delete !== 'n' && hookData.permissions.view;
@ -221,6 +213,50 @@ export const connectionsPageData: GenericPageData = {
} }
} }
], ],
// Custom action buttons (entity-specific)
customActions: [
{
id: 'connect',
icon: React.createElement(FaPlug),
title: 'connections.action.connect',
onClick: async (row: any, hookData: any) => {
if (hookData?.connectWithPopup) {
await hookData.connectWithPopup(row.id);
}
},
// Only show connect button if status is not 'active'
visible: (row: any) => row.status !== 'active',
disabled: (_row: any, hookData: any) => {
if (!hookData?.permissions) return { disabled: false, message: '' };
const hasUpdate = hookData.permissions.update !== 'n' && hookData.permissions.view;
return { disabled: !hasUpdate, message: 'No permission to connect' };
},
loading: (_row: any, hookData: any) => hookData?.isConnecting || false
},
{
id: 'refresh',
icon: React.createElement(FaSync),
title: 'connections.action.refresh',
onClick: async (row: any, hookData: any) => {
// Determine which refresh function to use based on authority
if (row.authority === 'msft' && hookData?.refreshMicrosoftToken) {
await hookData.refreshMicrosoftToken(row.id);
if (hookData?.refetch) await hookData.refetch();
} else if (row.authority === 'google' && hookData?.refreshGoogleToken) {
await hookData.refreshGoogleToken(row.id);
if (hookData?.refetch) await hookData.refetch();
}
},
// Only show refresh button if status is 'active' (already connected)
visible: (row: any) => row.status === 'active',
disabled: (_row: any, hookData: any) => {
if (!hookData?.permissions) return { disabled: false, message: '' };
const hasUpdate = hookData.permissions.update !== 'n' && hookData.permissions.view;
return { disabled: !hasUpdate, message: 'No permission to refresh token' };
},
loading: (row: any, hookData: any) => hookData?.refreshingConnections?.has(row.id) || false
}
],
searchable: true, searchable: true,
filterable: true, filterable: true,
sortable: true, sortable: true,

View file

@ -257,7 +257,7 @@ export const filesPageData: GenericPageData = {
await hookData.handleDownload(row.id, row.fileName, row.mimeType); await hookData.handleDownload(row.id, row.fileName, row.mimeType);
} }
}, },
disabled: (row: any, hookData: any) => { disabled: (_row: any, hookData: any) => {
if (!hookData?.permissions) return { disabled: false, message: '' }; if (!hookData?.permissions) return { disabled: false, message: '' };
const hasRead = hookData.permissions.read !== 'n' && hookData.permissions.view; const hasRead = hookData.permissions.read !== 'n' && hookData.permissions.view;
return { disabled: !hasRead, message: 'No permission to download files' }; return { disabled: !hasRead, message: 'No permission to download files' };

View file

@ -9,7 +9,7 @@ export type PrivilegeChecker = () => boolean | Promise<boolean>;
export interface ButtonFormField { export interface ButtonFormField {
key: string; key: string;
label: string | LanguageText; label: string | LanguageText;
type: 'string' | 'boolean' | 'email' | 'textarea' | 'date' | 'enum' | 'readonly' | 'multiselect'; type: 'string' | 'boolean' | 'email' | 'textarea' | 'date' | 'enum' | 'readonly' | 'multiselect' | 'number';
required?: boolean; required?: boolean;
placeholder?: string | LanguageText; placeholder?: string | LanguageText;
minRows?: number; minRows?: number;
@ -17,6 +17,7 @@ export interface ButtonFormField {
validator?: (value: any) => string | null; validator?: (value: any) => string | null;
defaultValue?: any; defaultValue?: any;
options?: string[] | Array<{ value: string | number; label: string }>; // For enum/multiselect fields options?: string[] | Array<{ value: string | number; label: string }>; // For enum/multiselect fields
optionsReference?: string; // Reference to a data source for dynamic options (e.g., 'TrusteeOrganisation', 'User')
} }
// Dropdown configuration for header dropdown buttons // Dropdown configuration for header dropdown buttons
@ -262,22 +263,26 @@ export interface GenericDataHook {
[key: string]: any; // Allow additional properties for dynamic data sources [key: string]: any; // Allow additional properties for dynamic data sources
} }
// Standard action button configuration (built-in actions: edit, delete, view, copy) // Standard action button configuration (built-in actions: edit, delete, view, copy, connect, play)
export interface ActionButtonConfig { export interface ActionButtonConfig {
type: 'view' | 'edit' | 'delete' | 'copy'; type: 'view' | 'edit' | 'delete' | 'copy' | 'connect' | 'play';
onAction?: (row: any) => Promise<void> | void; // Optional for delete buttons since they handle their own logic onAction?: (row: any) => Promise<void> | void; // Optional for delete buttons since they handle their own logic
title?: string | LanguageText; title?: string | LanguageText;
disabled?: (row: any) => boolean | { disabled: boolean; message?: string }; disabled?: (row: any, hookData?: any) => boolean | { disabled: boolean; message?: string };
loading?: (row: any) => boolean; loading?: (row: any, hookData?: any) => boolean;
// Field mappings for flexible data access // Field mappings for flexible data access
idField?: string; // Field name for the unique identifier (default: 'id') idField?: string; // Field name for the unique identifier (default: 'id')
nameField?: string; // Field name for display name (default: 'name' or 'file_name') 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') typeField?: string; // Field name for type/mime type (default: 'type' or 'mime_type')
contentField?: string; // Field name for content (default: 'content') contentField?: string; // Field name for content (default: 'content')
statusField?: string; // Field name for status (used by connect action)
// Operation and loading state names // Operation and loading state names
operationName?: string; // Name of the operation function in hookData operationName?: string; // Name of the operation function in hookData
loadingStateName?: string; // Name of the loading state in hookData loadingStateName?: string; // Name of the loading state in hookData
fetchItemFunctionName?: string; // Name of the function in hookData to fetch a single item by ID (for edit button) fetchItemFunctionName?: string; // Name of the function in hookData to fetch a single item by ID (for edit button)
// Navigation and mode (for play action)
navigateTo?: string; // Path to navigate to after action
mode?: string; // Mode to set (e.g., 'prompt', 'workflow')
} }
// Custom action button configuration (for entity-specific actions like download, connect, play, sendPasswordLink) // Custom action button configuration (for entity-specific actions like download, connect, play, sendPasswordLink)

View file

@ -1,6 +1,5 @@
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { useApiRequest } from './useApi'; import { useApiRequest } from './useApi';
import { getUserDataCache } from '../utils/userCache';
import api from '../api'; import api from '../api';
import { usePermissions, type UserPermissions } from './usePermissions'; import { usePermissions, type UserPermissions } from './usePermissions';
import { import {