diff --git a/src/api/billingApi.ts b/src/api/billingApi.ts index cc05e44..8094658 100644 --- a/src/api/billingApi.ts +++ b/src/api/billingApi.ts @@ -36,6 +36,29 @@ export interface BillingTransaction { userName?: string; } +/** Pagination request for GET /api/billing/transactions with `pagination` JSON (table + grouping). */ +export interface BillingTransactionsPaginationParams { + page?: number; + pageSize?: number; + sort?: Array<{ field: string; direction: 'asc' | 'desc' }>; + filters?: Record; + search?: string; + viewKey?: string; + groupByLevels?: Array<{ field: string; nullLabel?: string; direction?: 'asc' | 'desc' }>; +} + +export interface BillingTransactionsPaginatedResponse { + items: BillingTransaction[]; + pagination?: { + currentPage: number; + pageSize: number; + totalItems: number; + totalPages: number; + }; + groupLayout?: import('./connectionApi').GroupLayout; + appliedView?: { viewKey?: string; displayName?: string }; +} + export interface BillingSettings { id: string; mandateId: string; @@ -135,7 +158,31 @@ export async function fetchBalanceForMandate( } /** - * Fetch transaction history + * Fetch transaction history (table UI: pagination, filters, sort, saved views, grouping). + * Endpoint: GET /api/billing/transactions?pagination=... + */ +export async function fetchTransactionsPaginated( + request: ApiRequestFunction, + params?: BillingTransactionsPaginationParams +): Promise { + const paginationObj: Record = {}; + if (params?.page !== undefined) paginationObj.page = params.page; + if (params?.pageSize !== undefined) paginationObj.pageSize = params.pageSize; + if (params?.sort?.length) paginationObj.sort = params.sort; + if (params?.filters && Object.keys(params.filters).length > 0) paginationObj.filters = params.filters; + if (params?.search) paginationObj.search = params.search; + if (params?.viewKey) paginationObj.viewKey = params.viewKey; + if (params?.groupByLevels !== undefined) paginationObj.groupByLevels = params.groupByLevels; + + return await request({ + url: '/api/billing/transactions', + method: 'get', + params: { pagination: JSON.stringify(paginationObj) }, + }); +} + +/** + * Fetch transaction history (legacy array window) * Endpoint: GET /api/billing/transactions */ export async function fetchTransactions( diff --git a/src/api/connectionApi.ts b/src/api/connectionApi.ts index 41a79e4..8c47c6d 100644 --- a/src/api/connectionApi.ts +++ b/src/api/connectionApi.ts @@ -55,19 +55,22 @@ export interface PaginationParams { sort?: Array<{ field: string; direction: 'asc' | 'desc' }>; filters?: Record; search?: string; - /** Scope request to items of this group (resolved server-side to itemIds IN-filter). */ - groupId?: string; - /** If set, persist this group tree on the backend before fetching (optimistic save). */ - saveGroupTree?: TableGroupNode[]; + /** Key of a saved view to apply (server loads groupByLevels, filters, sort from DB). */ + viewKey?: string; + /** Explicit grouping levels; when sent (incl. []), overrides the view for this request. */ + groupByLevels?: Array<{ field: string; nullLabel?: string; direction?: 'asc' | 'desc' }>; } -export interface TableGroupNode { - id: string; - name: string; - itemIds: string[]; - subGroups: TableGroupNode[]; - order: number; - isExpanded: boolean; +export interface GroupBand { + path: string[]; + label: string; + startRowIndex: number; + rowCount: number; +} + +export interface GroupLayout { + levels: string[]; + bands: GroupBand[]; } export interface PaginatedResponse { @@ -78,8 +81,8 @@ export interface PaginatedResponse { totalItems: number; totalPages: number; }; - /** Current group tree for this (user, contextKey) pair — undefined if no grouping configured. */ - groupTree?: TableGroupNode[]; + groupLayout?: GroupLayout; + appliedView?: { viewKey?: string; displayName?: string }; } export interface CreateConnectionData { @@ -138,8 +141,8 @@ export async function fetchConnections( if (params.sort) paginationObj.sort = params.sort; if (params.filters) paginationObj.filters = params.filters; if (params.search) paginationObj.search = params.search; - if (params.groupId) paginationObj.groupId = params.groupId; - if (params.saveGroupTree !== undefined) paginationObj.saveGroupTree = params.saveGroupTree; + if (params.viewKey) paginationObj.viewKey = params.viewKey; + if (params.groupByLevels !== undefined) paginationObj.groupByLevels = params.groupByLevels; if (Object.keys(paginationObj).length > 0) { requestParams.pagination = JSON.stringify(paginationObj); diff --git a/src/api/fileApi.ts b/src/api/fileApi.ts index 7e2c67a..75151c3 100644 --- a/src/api/fileApi.ts +++ b/src/api/fileApi.ts @@ -34,8 +34,8 @@ export interface PaginationParams { sort?: Array<{ field: string; direction: 'asc' | 'desc' }>; filters?: Record; search?: string; - groupId?: string; - saveGroupTree?: any[]; + viewKey?: string; + groupByLevels?: Array<{ field: string; nullLabel?: string; direction?: 'asc' | 'desc' }>; } export interface PaginatedResponse { @@ -46,6 +46,8 @@ export interface PaginatedResponse { totalItems: number; totalPages: number; }; + groupLayout?: import('./connectionApi').GroupLayout; + appliedView?: { viewKey?: string; displayName?: string }; } // Type for the request function passed to API functions @@ -105,9 +107,9 @@ export async function fetchFiles( if (params.sort) paginationObj.sort = params.sort; if (params.filters) paginationObj.filters = params.filters; if (params.search) paginationObj.search = params.search; - if (params.groupId) paginationObj.groupId = params.groupId; - if (params.saveGroupTree !== undefined) paginationObj.saveGroupTree = params.saveGroupTree; - + if (params.viewKey) paginationObj.viewKey = params.viewKey; + if (params.groupByLevels !== undefined) paginationObj.groupByLevels = params.groupByLevels; + if (Object.keys(paginationObj).length > 0) { requestParams.pagination = JSON.stringify(paginationObj); } @@ -249,28 +251,13 @@ export async function deleteGroup( }); } -/** Collect all file IDs belonging to a group recursively (client-side, from known groupTree) */ +/** @deprecated Group tree removed — use view-based grouping (viewKey). Returns empty array. */ export function collectGroupItemIds( - groupTree: Array<{ id: string; itemIds: string[]; subGroups: any[] }>, - groupId: string + _groupTree: Array<{ id: string; itemIds: string[]; subGroups: any[] }>, + _groupId: string ): string[] { - const collect = (nodes: Array<{ id: string; itemIds: string[]; subGroups: any[] }>): string[] | null => { - for (const node of nodes) { - if (node.id === groupId) { - const ids: string[] = [...node.itemIds]; - const sub = (n: { id: string; itemIds: string[]; subGroups: any[] }) => { - ids.push(...n.itemIds); - n.subGroups.forEach(sub); - }; - node.subGroups.forEach(sub); - return ids; - } - const found = collect(node.subGroups); - if (found) return found; - } - return null; - }; - return collect(groupTree) ?? []; + const collect = (): string[] | null => null; + return collect() ?? []; } // Note: The following operations require special handling (FormData, blob responses) diff --git a/src/api/mandateApi.ts b/src/api/mandateApi.ts index 7946395..24aee62 100644 --- a/src/api/mandateApi.ts +++ b/src/api/mandateApi.ts @@ -46,8 +46,7 @@ export interface PaginationParams { sort?: Array<{ field: string; direction: 'asc' | 'desc' }>; filters?: Record; search?: string; - groupId?: string; - saveGroupTree?: any[]; + viewKey?: string; } export interface PaginatedResponse { @@ -86,8 +85,7 @@ export async function fetchMandates( if (params.sort) paginationObj.sort = params.sort; if (params.filters) paginationObj.filters = params.filters; if (params.search) paginationObj.search = params.search; - if (params.groupId) paginationObj.groupId = params.groupId; - if (params.saveGroupTree !== undefined) paginationObj.saveGroupTree = params.saveGroupTree; + if (params.viewKey) paginationObj.viewKey = params.viewKey; if (Object.keys(paginationObj).length > 0) { requestParams.pagination = JSON.stringify(paginationObj); diff --git a/src/api/promptApi.ts b/src/api/promptApi.ts index e735ae0..164a633 100644 --- a/src/api/promptApi.ts +++ b/src/api/promptApi.ts @@ -49,8 +49,8 @@ export interface PaginationParams { sort?: Array<{ field: string; direction: 'asc' | 'desc' }>; filters?: Record; search?: string; - groupId?: string; - saveGroupTree?: any[]; + viewKey?: string; + groupByLevels?: Array<{ field: string; nullLabel?: string; direction?: 'asc' | 'desc' }>; } export interface PaginatedResponse { @@ -61,6 +61,8 @@ export interface PaginatedResponse { totalItems: number; totalPages: number; }; + groupLayout?: import('./connectionApi').GroupLayout; + appliedView?: { viewKey?: string; displayName?: string }; } export interface CreatePromptData { @@ -112,9 +114,9 @@ export async function fetchPrompts( if (params.sort) paginationObj.sort = params.sort; if (params.filters) paginationObj.filters = params.filters; if (params.search) paginationObj.search = params.search; - if (params.groupId) paginationObj.groupId = params.groupId; - if (params.saveGroupTree !== undefined) paginationObj.saveGroupTree = params.saveGroupTree; - + if (params.viewKey) paginationObj.viewKey = params.viewKey; + if (params.groupByLevels !== undefined) paginationObj.groupByLevels = params.groupByLevels; + if (Object.keys(paginationObj).length > 0) { requestParams.pagination = JSON.stringify(paginationObj); } diff --git a/src/api/tableViewApi.ts b/src/api/tableViewApi.ts new file mode 100644 index 0000000..08039e8 --- /dev/null +++ b/src/api/tableViewApi.ts @@ -0,0 +1,59 @@ +import api from '../api'; + +export interface TableListViewRow { + id: string; + userId?: string; + mandateId?: string | null; + contextKey: string; + viewKey: string; + displayName: string; + config: TableViewConfig; + updatedAt?: number; +} + +export interface TableViewConfig { + schemaVersion?: number; + filters?: Record; + sort?: Array<{ field: string; direction: 'asc' | 'desc' }>; + groupByLevels?: Array<{ field: string; nullLabel?: string }>; + /** Section mode (`tableGroupLayoutMode="sections"`): stable keys (`sk`) of collapsed sections. */ + collapsedSectionKeys?: string[]; + /** Inline `groupLayout` bands: keys are `band.path.join('///')`. */ + collapsedGroupKeys?: string[]; +} + +export async function listTableViews(contextKey: string): Promise { + const { data } = await api.get('/api/table-views', { + params: { contextKey }, + }); + return Array.isArray(data) ? data : []; +} + +export async function getTableView(contextKey: string, viewKey: string): Promise { + const { data } = await api.get(`/api/table-views/${encodeURIComponent(viewKey)}`, { + params: { contextKey }, + }); + return data; +} + +export async function createTableView(payload: { + contextKey: string; + viewKey: string; + displayName: string; + config: TableViewConfig; +}): Promise { + const { data } = await api.post('/api/table-views', payload); + return data; +} + +export async function updateTableView( + viewId: string, + updates: { displayName?: string; viewKey?: string; config?: TableViewConfig }, +): Promise { + const { data } = await api.put(`/api/table-views/${encodeURIComponent(viewId)}`, updates); + return data; +} + +export async function deleteTableView(viewId: string): Promise { + await api.delete(`/api/table-views/${encodeURIComponent(viewId)}`); +} diff --git a/src/api/userApi.ts b/src/api/userApi.ts index 98dd7a2..3615375 100644 --- a/src/api/userApi.ts +++ b/src/api/userApi.ts @@ -48,8 +48,7 @@ export interface PaginationParams { sort?: Array<{ field: string; direction: 'asc' | 'desc' }>; filters?: Record; search?: string; - groupId?: string; - saveGroupTree?: any[]; + viewKey?: string; } export interface PaginatedResponse { @@ -154,8 +153,7 @@ export async function fetchUsers( if (params.sort) paginationObj.sort = params.sort; if (params.filters) paginationObj.filters = params.filters; if (params.search) paginationObj.search = params.search; - if (params.groupId) paginationObj.groupId = params.groupId; - if (params.saveGroupTree !== undefined) paginationObj.saveGroupTree = params.saveGroupTree; + if (params.viewKey) paginationObj.viewKey = params.viewKey; if (Object.keys(paginationObj).length > 0) { requestParams.pagination = JSON.stringify(paginationObj); diff --git a/src/api/workflowApi.ts b/src/api/workflowApi.ts index 26e2367..7a03b4e 100644 --- a/src/api/workflowApi.ts +++ b/src/api/workflowApi.ts @@ -30,6 +30,8 @@ export interface PortField { description: string | Record; required: boolean; enumValues?: string[] | null; + /** When true, surface at the top of the DataPicker as the most common/recommended pick. */ + recommended?: boolean; } export interface PortSchema { @@ -85,11 +87,19 @@ export interface SystemVariable { description: string; } +/** Single form field type with its canonical port primitive. Delivered by GET /node-types. */ +export interface FormFieldType { + id: string; + label: string; + portType: string; +} + export interface NodeTypesResponse { nodeTypes: NodeType[]; categories: NodeTypeCategory[]; portTypeCatalog?: Record; systemVariables?: Record; + formFieldTypes?: FormFieldType[]; } export interface Automation2GraphNode { @@ -279,12 +289,14 @@ export async function fetchNodeTypes( const categories = data?.categories ?? []; const portTypeCatalog = data?.portTypeCatalog ?? undefined; const systemVariables = data?.systemVariables ?? undefined; + const formFieldTypes = data?.formFieldTypes ?? undefined; console.log( `${LOG} fetchNodeTypes response: ${nodeTypes.length} nodeTypes, ${categories.length} categories, ` + `${portTypeCatalog ? Object.keys(portTypeCatalog).length : 0} portTypes, ` + - `${systemVariables ? Object.keys(systemVariables).length : 0} sysVars` + `${systemVariables ? Object.keys(systemVariables).length : 0} sysVars, ` + + `${formFieldTypes ? formFieldTypes.length : 0} formFieldTypes` ); - return { nodeTypes, categories, portTypeCatalog, systemVariables }; + return { nodeTypes, categories, portTypeCatalog, systemVariables, formFieldTypes }; } export interface UpstreamPathEntry { diff --git a/src/components/FlowEditor/context/Automation2DataFlowContext.tsx b/src/components/FlowEditor/context/Automation2DataFlowContext.tsx index 8be4ea9..f36f87c 100644 --- a/src/components/FlowEditor/context/Automation2DataFlowContext.tsx +++ b/src/components/FlowEditor/context/Automation2DataFlowContext.tsx @@ -6,7 +6,7 @@ import React, { createContext, useContext, useMemo } from 'react'; import type { CanvasNode, CanvasConnection } from '../editor/FlowCanvas'; import { getAvailableSources } from '../nodes/shared/dataFlowGraph'; -import type { ApiRequestFunction, NodeType, PortField, PortSchema, SystemVariable } from '../../../api/workflowApi'; +import type { ApiRequestFunction, FormFieldType, NodeType, PortField, PortSchema, SystemVariable } from '../../../api/workflowApi'; export interface Automation2DataFlowContextValue { currentNodeId: string; @@ -17,6 +17,8 @@ export interface Automation2DataFlowContextValue { language: string; portTypeCatalog: Record; systemVariables: Record; + /** Canonical form field types from the API — maps UI type id to portType primitive. */ + formFieldTypes: FormFieldType[]; getNodeLabel: (node: { id: string; title?: string; label?: string; type?: string }) => string; getAvailableSourceIds: () => string[]; /** Present when rendered inside the flow editor (ConnectionPicker / tools). */ @@ -41,6 +43,7 @@ interface Automation2DataFlowProviderProps { language: string; portTypeCatalog?: Record; systemVariables?: Record; + formFieldTypes?: FormFieldType[]; instanceId?: string; request?: ApiRequestFunction; children: React.ReactNode; @@ -55,12 +58,18 @@ export const Automation2DataFlowProvider: React.FC { const value = useMemo((): Automation2DataFlowContextValue | null => { if (!node) return null; + const formTypeToPort: Record = Object.fromEntries( + formFieldTypes.map((f) => [f.id, f.portType]) + ); + const resolvePortType = (rawType: string): string => formTypeToPort[rawType] ?? rawType; + const parseGraphDefinedSchema = (parameterKey: string): PortSchema | null => { const raw = node.parameters?.[parameterKey]; if (!Array.isArray(raw)) return null; @@ -72,8 +81,8 @@ export const Automation2DataFlowProvider: React.FC).de ?? '') : ''; - const ftype = typeof rec.type === 'string' ? rec.type : 'str'; - if (ftype === 'group' && Array.isArray(rec.fields)) { + const rawType = typeof rec.type === 'string' ? rec.type : 'str'; + if (rawType === 'group' && Array.isArray(rec.fields)) { for (const sub of rec.fields as Record[]) { if (!sub || typeof sub.name !== 'string') continue; const sl = sub.label; @@ -85,7 +94,7 @@ export const Automation2DataFlowProvider: React.FC n.title ?? n.label ?? n.type ?? n.id, getAvailableSourceIds: () => getAvailableSources(node.id, nodes, connections), @@ -117,7 +127,7 @@ export const Automation2DataFlowProvider: React.FC diff --git a/src/components/FlowEditor/editor/Automation2FlowEditor.module.css b/src/components/FlowEditor/editor/Automation2FlowEditor.module.css index 67a4261..1b32bfb 100644 --- a/src/components/FlowEditor/editor/Automation2FlowEditor.module.css +++ b/src/components/FlowEditor/editor/Automation2FlowEditor.module.css @@ -1725,6 +1725,35 @@ margin-left: 4px; } +/* Type-mismatch warning badge (⚠) — shown instead of hiding incompatible fields. */ +.dataPickerMismatchBadge { + font-size: 10px; + margin-left: 4px; + color: var(--color-warning, #f59e0b); + flex-shrink: 0; +} + +/* Recommended pick: subtle highlight on the row */ +.dataPickerLeafRecommended { + font-weight: 500; +} + +/* "Empfohlen" pill shown on recommended entries */ +.dataPickerRecommendedPill { + display: inline-block; + font-size: 9px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.03em; + padding: 1px 5px; + border-radius: 10px; + margin-left: 5px; + background: var(--color-primary-light, #dbeafe); + color: var(--color-primary, #2563eb); + flex-shrink: 0; + vertical-align: middle; +} + /* "iterieren" affordance — visually distinct (subtle accent), readable on * the picker's white background and on the leaf's blue hover background. */ .dataPickerIterateBtn { diff --git a/src/components/FlowEditor/editor/Automation2FlowEditor.tsx b/src/components/FlowEditor/editor/Automation2FlowEditor.tsx index f2ddf7a..772e9df 100644 --- a/src/components/FlowEditor/editor/Automation2FlowEditor.tsx +++ b/src/components/FlowEditor/editor/Automation2FlowEditor.tsx @@ -99,6 +99,7 @@ export const Automation2FlowEditor: React.FC = ({ in const [categories, setCategories] = useState([]); const [portTypeCatalog, setPortTypeCatalog] = useState>({}); const [systemVariables, setSystemVariables] = useState>({}); + const [formFieldTypes, setFormFieldTypes] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [filter, setFilter] = useState(''); @@ -459,6 +460,7 @@ export const Automation2FlowEditor: React.FC = ({ in setRegistryCatalog(data.portTypeCatalog as never); } if (data.systemVariables) setSystemVariables(data.systemVariables); + if (data.formFieldTypes) setFormFieldTypes(data.formFieldTypes); } catch (err: unknown) { setError(err instanceof Error ? err.message : String(err)); setNodeTypes([]); @@ -904,6 +906,7 @@ export const Automation2FlowEditor: React.FC = ({ in language={language} portTypeCatalog={portTypeCatalog as Record} systemVariables={systemVariables as Record} + formFieldTypes={formFieldTypes} instanceId={instanceId} request={request} > diff --git a/src/components/FlowEditor/nodes/form/FormNodeConfig.tsx b/src/components/FlowEditor/nodes/form/FormNodeConfig.tsx index fbb8dc3..d0a07f6 100644 --- a/src/components/FlowEditor/nodes/form/FormNodeConfig.tsx +++ b/src/components/FlowEditor/nodes/form/FormNodeConfig.tsx @@ -7,11 +7,16 @@ import { FaGripVertical, FaTimes } from 'react-icons/fa'; import type { FormField, NodeConfigRendererProps } from '../shared/types'; import { FORM_FIELD_TYPES, FORM_FIELD_TYPE_LABELS } from '../../../../utils/attributeTypeMapper'; import styles from '../../editor/Automation2FlowEditor.module.css'; +import { useAutomation2DataFlow } from '../../context/Automation2DataFlowContext'; import { useLanguage } from '../../../../providers/language/LanguageContext'; export const FormNodeConfig: React.FC = ({ params, updateParam }) => { const { t } = useLanguage(); + const ctx = useAutomation2DataFlow(); + const fieldTypeOptions = ctx?.formFieldTypes?.length + ? ctx.formFieldTypes + : FORM_FIELD_TYPES.map((ft) => ({ id: ft, label: FORM_FIELD_TYPE_LABELS[ft] ?? ft, portType: 'str' })); const fields = (params.fields as FormField[]) ?? []; const moveField = (fromIndex: number, toIndex: number) => { @@ -88,8 +93,8 @@ export const FormNodeConfig: React.FC = ({ params, upda }} style={{ width: 'auto', minWidth: 90 }} > - {FORM_FIELD_TYPES.map(ft => ( - + {fieldTypeOptions.map((ft) => ( + ))}