From 0d40eecbe7e5544372713ad51917d8719b2937f6 Mon Sep 17 00:00:00 2001 From: thanhtunguet Date: Sat, 31 Jan 2026 21:47:49 +0700 Subject: [PATCH] refactor(ModelMappingDiagram): restructure component to utilize new modular columns and improve type definitions --- .../modelAlias/ModelMappingDiagram.tsx | 573 ++++-------------- .../modelAlias/ModelMappingDiagramColumns.tsx | 229 +++++++ .../ModelMappingDiagramContextMenu.tsx | 111 ++++ .../modelAlias/ModelMappingDiagramModals.tsx | 267 ++++++++ .../modelAlias/ModelMappingDiagramTypes.ts | 33 + 5 files changed, 757 insertions(+), 456 deletions(-) create mode 100644 src/components/modelAlias/ModelMappingDiagramColumns.tsx create mode 100644 src/components/modelAlias/ModelMappingDiagramContextMenu.tsx create mode 100644 src/components/modelAlias/ModelMappingDiagramModals.tsx create mode 100644 src/components/modelAlias/ModelMappingDiagramTypes.ts diff --git a/src/components/modelAlias/ModelMappingDiagram.tsx b/src/components/modelAlias/ModelMappingDiagram.tsx index 0085c17..0425ba9 100644 --- a/src/components/modelAlias/ModelMappingDiagram.tsx +++ b/src/components/modelAlias/ModelMappingDiagram.tsx @@ -1,23 +1,24 @@ import { forwardRef, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, type DragEvent, type MouseEvent as ReactMouseEvent } from 'react'; -import { createPortal } from 'react-dom'; import { useTranslation } from 'react-i18next'; import type { OAuthModelAliasEntry } from '@/types'; import { useThemeStore } from '@/stores'; -import { Modal } from '@/components/ui/Modal'; -import { Input } from '@/components/ui/Input'; -import { Button } from '@/components/ui/Button'; -import { ToggleSwitch } from '@/components/ui/ToggleSwitch'; -import { IconTrash2 } from '@/components/ui/icons'; +import { AliasColumn, ProviderColumn, SourceColumn } from './ModelMappingDiagramColumns'; +import { DiagramContextMenu } from './ModelMappingDiagramContextMenu'; +import { + AddAliasModal, + RenameAliasModal, + SettingsAliasModal, + SettingsSourceModal +} from './ModelMappingDiagramModals'; +import type { + AliasNode, + AuthFileModelItem, + ContextMenuState, + DiagramLine, + SourceNode +} from './ModelMappingDiagramTypes'; import styles from './ModelMappingDiagram.module.scss'; -// Type definition for models from API -export interface AuthFileModelItem { - id: string; - display_name?: string; - type?: string; - owned_by?: string; -} - export interface ModelMappingDiagramProps { modelAlias: Record; allProviderModels?: Record; @@ -31,7 +32,6 @@ export interface ModelMappingDiagramProps { className?: string; } -// Helper to generate consistent colors const PROVIDER_COLORS = [ '#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899', '#06b6d4', '#84cc16' @@ -42,26 +42,6 @@ function getProviderColor(provider: string): string { return PROVIDER_COLORS[hash % PROVIDER_COLORS.length]; } -interface SourceNode { - id: string; // unique: provider::name - provider: string; - name: string; - aliases: { alias: string; fork: boolean }[]; // all aliases this source maps to -} - -interface AliasNode { - id: string; // alias - alias: string; - sources: SourceNode[]; -} - -interface ContextMenuState { - x: number; - y: number; - type: 'alias' | 'background' | 'provider' | 'source'; - data?: string; -} - export interface ModelMappingDiagramRef { collapseAll: () => void; refreshLayout: () => void; @@ -84,7 +64,7 @@ export const ModelMappingDiagram = forwardRef(null); - const [lines, setLines] = useState<{ path: string; color: string; id: string }[]>([]); + const [lines, setLines] = useState([]); const [draggedSource, setDraggedSource] = useState(null); const [draggedAlias, setDraggedAlias] = useState(null); const [dropTargetAlias, setDropTargetAlias] = useState(null); @@ -100,18 +80,6 @@ export const ModelMappingDiagram = forwardRef(null); const [settingsSourceId, setSettingsSourceId] = useState(null); - const contextMenuRef = useRef(null); - - // Close context menu on click outside - useEffect(() => { - const handleClick = (event: globalThis.MouseEvent) => { - if (!contextMenuRef.current?.contains(event.target as Node)) { - setContextMenu(null); - } - }; - document.addEventListener('mousedown', handleClick); - return () => document.removeEventListener('mousedown', handleClick); - }, []); // Parse data: each source model (provider+name) and each alias is distinct by id; 1 source -> many aliases. const { aliasNodes, providerNodes } = useMemo(() => { @@ -458,77 +426,6 @@ export const ModelMappingDiagram = forwardRef { - const aliasData = contextMenu?.data; - if (contextMenu?.type !== 'alias' || aliasData == null) return null; - return ( - <> -
handleRenameClick(aliasData)}> - {t('oauth_model_alias.diagram_rename')} -
-
{ - closeContextMenu(); - setSettingsAlias(aliasData); - }} - > - {t('oauth_model_alias.diagram_settings')} -
-
-
handleDeleteClick(aliasData)}> - {t('oauth_model_alias.diagram_delete_alias')} -
- - ); - }; - - const renderProviderMenu = () => { - const provider = contextMenu?.data; - if (contextMenu?.type !== 'provider' || !provider) return null; - return ( - <> -
{ - closeContextMenu(); - onEditProvider?.(provider); - }} - > - {t('common.edit')} -
-
-
{ - closeContextMenu(); - onDeleteProvider?.(provider); - }} - > - {t('oauth_model_alias.delete')} -
- - ); - }; - - const renderSourceMenu = () => { - const sourceId = contextMenu?.data; - const source = resolveSourceById(sourceId ?? null); - if (contextMenu?.type !== 'source' || !source) return null; - return ( - <> -
{ - closeContextMenu(); - setSettingsSourceId(source.id); - }} - > - {t('oauth_model_alias.diagram_settings')} -
- - ); - }; return (
- {/* Column 1: Providers (mindmap root branch) */} -
{ - e.preventDefault(); - e.stopPropagation(); - handleContextMenu(e, 'background'); + handleContextMenu(e, type, data)} + label={t('oauth_model_alias.diagram_providers')} + expandLabel={t('oauth_model_alias.diagram_expand')} + collapseLabel={t('oauth_model_alias.diagram_collapse')} + /> + { + setDraggedSource(null); + setDropTargetAlias(null); }} - > -
{t('oauth_model_alias.diagram_providers')}
- {providerNodes.map(({ provider, sources }) => { - const collapsed = collapsedProviders.has(provider); - return ( -
{ - if (el) providerRefs.current.set(provider, el); - else providerRefs.current.delete(provider); - }} - className={`${styles.item} ${styles.providerItem}`} - style={{ borderLeftColor: getProviderColor(provider) }} - onContextMenu={(e) => { - e.preventDefault(); - e.stopPropagation(); - handleContextMenu(e, 'provider', provider); - }} - > - - - {provider} - - {sources.length} -
- ); - })} -
- - {/* Column 2: Source Models (children per provider; hidden when provider collapsed) */} -
{ - e.preventDefault(); - e.stopPropagation(); - handleContextMenu(e, 'background'); + onDragOver={handleDragOverSource} + onDragLeave={handleDragLeaveSource} + onDrop={handleDropOnSource} + onContextMenu={(e, type, data) => handleContextMenu(e, type, data)} + label={t('oauth_model_alias.diagram_source_models')} + /> + { + setDraggedAlias(null); + setDropTargetSource(null); }} - > -
{t('oauth_model_alias.diagram_source_models')}
- {providerNodes.flatMap(({ provider, sources }) => { - if (collapsedProviders.has(provider)) return []; - return sources.map((source) => ( -
{ - if (el) sourceRefs.current.set(source.id, el); - else sourceRefs.current.delete(source.id); - }} - className={`${styles.item} ${styles.sourceItem} ${ - draggedSource?.id === source.id ? styles.dragging : '' - } ${dropTargetSource === source.id ? styles.dropTarget : ''}`} - draggable={!!onUpdate} - onDragStart={(e) => handleDragStart(e, source)} - onDragEnd={() => { - setDraggedSource(null); - setDropTargetAlias(null); - }} - onDragOver={(e) => handleDragOverSource(e, source)} - onDragLeave={handleDragLeaveSource} - onDrop={(e) => handleDropOnSource(e, source)} - onContextMenu={(e) => { - e.preventDefault(); - e.stopPropagation(); - handleContextMenu(e, 'source', source.id); - }} - > - - {source.name} - -
0 ? 1 : 0.3 - }} - /> -
- )); - })} -
+ onDragOver={handleDragOver} + onDragLeave={handleDragLeave} + onDrop={handleDrop} + onContextMenu={(e, type, data) => handleContextMenu(e, type, data)} + label={t('oauth_model_alias.diagram_aliases')} + /> -
{ - e.preventDefault(); - e.stopPropagation(); - handleContextMenu(e, 'background'); + setContextMenu(null)} + onAddAlias={handleAddAlias} + onRenameAlias={handleRenameClick} + onOpenAliasSettings={(alias) => { + setContextMenu(null); + setSettingsAlias(alias); }} - > -
{t('oauth_model_alias.diagram_aliases')}
- {aliasNodes.map((node) => ( -
{ - if (el) aliasRefs.current.set(node.id, el); - else aliasRefs.current.delete(node.id); - }} - className={`${styles.item} ${styles.aliasItem} ${ - dropTargetAlias === node.alias ? styles.dropTarget : '' - } ${draggedAlias === node.alias ? styles.dragging : ''}`} - draggable={!!onUpdate} - onDragStart={(e) => handleDragStartAlias(e, node.alias)} - onDragEnd={() => { - setDraggedAlias(null); - setDropTargetSource(null); - }} - onDragOver={(e) => handleDragOver(e, node.alias)} - onDragLeave={handleDragLeave} - onDrop={(e) => handleDrop(e, node.alias)} - onContextMenu={(e) => { - e.preventDefault(); - e.stopPropagation(); - handleContextMenu(e, 'alias', node.alias); - }} - > -
- - {node.alias} - - {node.sources.length} -
- ))} -
+ onDeleteAlias={handleDeleteClick} + onEditProvider={(provider) => { + setContextMenu(null); + onEditProvider?.(provider); + }} + onDeleteProvider={(provider) => { + setContextMenu(null); + onDeleteProvider?.(provider); + }} + onOpenSourceSettings={(sourceId) => { + setContextMenu(null); + setSettingsSourceId(sourceId); + }} + /> - {contextMenu && - createPortal( -
e.stopPropagation()} - > - {contextMenu.type === 'background' && ( -
- {t('oauth_model_alias.diagram_add_alias')} -
- )} - {contextMenu.type === 'alias' && renderAliasMenu()} - {contextMenu.type === 'provider' && renderProviderMenu()} - {contextMenu.type === 'source' && renderSourceMenu()} -
, - document.body - )} - - { + setRenameValue(value); + setRenameError(''); + }} onClose={() => setRenameState(null)} - title={t('oauth_model_alias.diagram_rename_alias_title')} - width={400} - footer={ - <> - - - - } - > - { - setRenameValue(e.target.value); - setRenameError(''); - }} - onKeyDown={(e) => { - if (e.key === 'Enter') handleRenameSubmit(); - }} - error={renameError} - placeholder={t('oauth_model_alias.diagram_rename_placeholder')} - autoFocus - /> - - - + { + setAddAliasValue(value); + setAddAliasError(''); + }} onClose={() => setAddAliasOpen(false)} - title={t('oauth_model_alias.diagram_add_alias_title')} - width={400} - footer={ - <> - - - - } - > - { - setAddAliasValue(e.target.value); - setAddAliasError(''); - }} - onKeyDown={(e) => { - if (e.key === 'Enter') handleAddAliasSubmit(); - }} - error={addAliasError} - placeholder={t('oauth_model_alias.diagram_add_placeholder')} - autoFocus - /> - - - + setSettingsAlias(null)} - title={t('oauth_model_alias.diagram_settings_title', { alias: settingsAlias ?? '' })} - width={720} - footer={ - - } - > - {settingsAlias ? ( - (() => { - const node = aliasNodes.find((n) => n.alias === settingsAlias); - if (!node || node.sources.length === 0) { - return
{t('oauth_model_alias.diagram_settings_empty')}
; - } - return ( -
- {node.sources.map((source) => { - const entry = source.aliases.find((item) => item.alias === settingsAlias); - const forkEnabled = entry?.fork === true; - return ( -
-
- {source.name} - - {settingsAlias} -
-
- - {t('oauth_model_alias.alias_fork_label')} - - handleToggleFork(source.provider, source.name, settingsAlias, value)} - ariaLabel={t('oauth_model_alias.alias_fork_label')} - /> - -
-
- ); - })} -
- ); - })() - ) : null} -
- - + setSettingsSourceId(null)} - title={t('oauth_model_alias.diagram_settings_source_title')} - width={720} - footer={ - - } - > - {settingsSourceId ? ( - (() => { - const source = resolveSourceById(settingsSourceId); - if (!source || source.aliases.length === 0) { - return
{t('oauth_model_alias.diagram_settings_empty')}
; - } - return ( -
- {source.aliases.map((entry) => ( -
-
- {source.name} - - {entry.alias} -
-
- - {t('oauth_model_alias.alias_fork_label')} - - - handleToggleFork(source.provider, source.name, entry.alias, value) - } - ariaLabel={t('oauth_model_alias.alias_fork_label')} - /> - -
-
- ))} -
- ); - })() - ) : null} -
+ onToggleFork={handleToggleFork} + onUnlink={handleUnlinkSource} + />
); }); diff --git a/src/components/modelAlias/ModelMappingDiagramColumns.tsx b/src/components/modelAlias/ModelMappingDiagramColumns.tsx new file mode 100644 index 0000000..eaf7d95 --- /dev/null +++ b/src/components/modelAlias/ModelMappingDiagramColumns.tsx @@ -0,0 +1,229 @@ +import type { DragEvent, MouseEvent as ReactMouseEvent, RefObject } from 'react'; +import type { AliasNode, ProviderNode, SourceNode } from './ModelMappingDiagramTypes'; +import styles from './ModelMappingDiagram.module.scss'; + +interface ProviderColumnProps { + providerNodes: ProviderNode[]; + collapsedProviders: Set; + getProviderColor: (provider: string) => string; + providerRefs: RefObject>; + onToggleCollapse: (provider: string) => void; + onContextMenu: (e: ReactMouseEvent, type: 'provider' | 'background', data?: string) => void; + label: string; + expandLabel: string; + collapseLabel: string; +} + +export function ProviderColumn({ + providerNodes, + collapsedProviders, + getProviderColor, + providerRefs, + onToggleCollapse, + onContextMenu, + label, + expandLabel, + collapseLabel +}: ProviderColumnProps) { + return ( +
{ + e.preventDefault(); + e.stopPropagation(); + onContextMenu(e, 'background'); + }} + > +
{label}
+ {providerNodes.map(({ provider, sources }) => { + const collapsed = collapsedProviders.has(provider); + return ( +
{ + if (el) providerRefs.current?.set(provider, el); + else providerRefs.current?.delete(provider); + }} + className={`${styles.item} ${styles.providerItem}`} + style={{ borderLeftColor: getProviderColor(provider) }} + onContextMenu={(e) => { + e.preventDefault(); + e.stopPropagation(); + onContextMenu(e, 'provider', provider); + }} + > + + + {provider} + + {sources.length} +
+ ); + })} +
+ ); +} + +interface SourceColumnProps { + providerNodes: ProviderNode[]; + collapsedProviders: Set; + sourceRefs: RefObject>; + getProviderColor: (provider: string) => string; + draggedSource: SourceNode | null; + dropTargetSource: string | null; + draggable: boolean; + onDragStart: (e: DragEvent, source: SourceNode) => void; + onDragEnd: () => void; + onDragOver: (e: DragEvent, source: SourceNode) => void; + onDragLeave: () => void; + onDrop: (e: DragEvent, source: SourceNode) => void; + onContextMenu: (e: ReactMouseEvent, type: 'source' | 'background', data?: string) => void; + label: string; +} + +export function SourceColumn({ + providerNodes, + collapsedProviders, + sourceRefs, + getProviderColor, + draggedSource, + dropTargetSource, + draggable, + onDragStart, + onDragEnd, + onDragOver, + onDragLeave, + onDrop, + onContextMenu, + label +}: SourceColumnProps) { + return ( +
{ + e.preventDefault(); + e.stopPropagation(); + onContextMenu(e, 'background'); + }} + > +
{label}
+ {providerNodes.flatMap(({ provider, sources }) => { + if (collapsedProviders.has(provider)) return []; + return sources.map((source) => ( +
{ + if (el) sourceRefs.current?.set(source.id, el); + else sourceRefs.current?.delete(source.id); + }} + className={`${styles.item} ${styles.sourceItem} ${ + draggedSource?.id === source.id ? styles.dragging : '' + } ${dropTargetSource === source.id ? styles.dropTarget : ''}`} + draggable={draggable} + onDragStart={(e) => onDragStart(e, source)} + onDragEnd={onDragEnd} + onDragOver={(e) => onDragOver(e, source)} + onDragLeave={onDragLeave} + onDrop={(e) => onDrop(e, source)} + onContextMenu={(e) => { + e.preventDefault(); + e.stopPropagation(); + onContextMenu(e, 'source', source.id); + }} + > + + {source.name} + +
0 ? 1 : 0.3 + }} + /> +
+ )); + })} +
+ ); +} + +interface AliasColumnProps { + aliasNodes: AliasNode[]; + aliasRefs: RefObject>; + dropTargetAlias: string | null; + draggedAlias: string | null; + draggable: boolean; + onDragStart: (e: DragEvent, alias: string) => void; + onDragEnd: () => void; + onDragOver: (e: DragEvent, alias: string) => void; + onDragLeave: () => void; + onDrop: (e: DragEvent, alias: string) => void; + onContextMenu: (e: ReactMouseEvent, type: 'alias' | 'background', data?: string) => void; + label: string; +} + +export function AliasColumn({ + aliasNodes, + aliasRefs, + dropTargetAlias, + draggedAlias, + draggable, + onDragStart, + onDragEnd, + onDragOver, + onDragLeave, + onDrop, + onContextMenu, + label +}: AliasColumnProps) { + return ( +
{ + e.preventDefault(); + e.stopPropagation(); + onContextMenu(e, 'background'); + }} + > +
{label}
+ {aliasNodes.map((node) => ( +
{ + if (el) aliasRefs.current?.set(node.id, el); + else aliasRefs.current?.delete(node.id); + }} + className={`${styles.item} ${styles.aliasItem} ${ + dropTargetAlias === node.alias ? styles.dropTarget : '' + } ${draggedAlias === node.alias ? styles.dragging : ''}`} + draggable={draggable} + onDragStart={(e) => onDragStart(e, node.alias)} + onDragEnd={onDragEnd} + onDragOver={(e) => onDragOver(e, node.alias)} + onDragLeave={onDragLeave} + onDrop={(e) => onDrop(e, node.alias)} + onContextMenu={(e) => { + e.preventDefault(); + e.stopPropagation(); + onContextMenu(e, 'alias', node.alias); + }} + > +
+ + {node.alias} + + {node.sources.length} +
+ ))} +
+ ); +} diff --git a/src/components/modelAlias/ModelMappingDiagramContextMenu.tsx b/src/components/modelAlias/ModelMappingDiagramContextMenu.tsx new file mode 100644 index 0000000..390ccfc --- /dev/null +++ b/src/components/modelAlias/ModelMappingDiagramContextMenu.tsx @@ -0,0 +1,111 @@ +import { useEffect, useRef } from 'react'; +import { createPortal } from 'react-dom'; +import type { TFunction } from 'i18next'; +import type { ContextMenuState } from './ModelMappingDiagramTypes'; +import styles from './ModelMappingDiagram.module.scss'; + +interface DiagramContextMenuProps { + contextMenu: ContextMenuState | null; + t: TFunction; + onRequestClose: () => void; + onAddAlias: () => void; + onRenameAlias: (alias: string) => void; + onOpenAliasSettings: (alias: string) => void; + onDeleteAlias: (alias: string) => void; + onEditProvider: (provider: string) => void; + onDeleteProvider: (provider: string) => void; + onOpenSourceSettings: (sourceId: string) => void; +} + +export function DiagramContextMenu({ + contextMenu, + t, + onRequestClose, + onAddAlias, + onRenameAlias, + onOpenAliasSettings, + onDeleteAlias, + onEditProvider, + onDeleteProvider, + onOpenSourceSettings +}: DiagramContextMenuProps) { + const menuRef = useRef(null); + + useEffect(() => { + if (!contextMenu) return; + const handleClick = (event: globalThis.MouseEvent) => { + if (!menuRef.current?.contains(event.target as Node)) { + onRequestClose(); + } + }; + document.addEventListener('mousedown', handleClick); + return () => document.removeEventListener('mousedown', handleClick); + }, [contextMenu, onRequestClose]); + + if (!contextMenu) return null; + + const { type, data } = contextMenu; + + const renderBackground = () => ( +
+ {t('oauth_model_alias.diagram_add_alias')} +
+ ); + + const renderAlias = () => { + if (!data) return null; + return ( + <> +
onRenameAlias(data)}> + {t('oauth_model_alias.diagram_rename')} +
+
onOpenAliasSettings(data)}> + {t('oauth_model_alias.diagram_settings')} +
+
+
onDeleteAlias(data)}> + {t('oauth_model_alias.diagram_delete_alias')} +
+ + ); + }; + + const renderProvider = () => { + if (!data) return null; + return ( + <> +
onEditProvider(data)}> + {t('common.edit')} +
+
+
onDeleteProvider(data)}> + {t('oauth_model_alias.delete')} +
+ + ); + }; + + const renderSource = () => { + if (!data) return null; + return ( +
onOpenSourceSettings(data)}> + {t('oauth_model_alias.diagram_settings')} +
+ ); + }; + + return createPortal( +
e.stopPropagation()} + > + {type === 'background' && renderBackground()} + {type === 'alias' && renderAlias()} + {type === 'provider' && renderProvider()} + {type === 'source' && renderSource()} +
, + document.body + ); +} diff --git a/src/components/modelAlias/ModelMappingDiagramModals.tsx b/src/components/modelAlias/ModelMappingDiagramModals.tsx new file mode 100644 index 0000000..6f8e2fb --- /dev/null +++ b/src/components/modelAlias/ModelMappingDiagramModals.tsx @@ -0,0 +1,267 @@ +import type { KeyboardEvent } from 'react'; +import type { TFunction } from 'i18next'; +import { Modal } from '@/components/ui/Modal'; +import { Input } from '@/components/ui/Input'; +import { Button } from '@/components/ui/Button'; +import { ToggleSwitch } from '@/components/ui/ToggleSwitch'; +import { IconTrash2 } from '@/components/ui/icons'; +import type { AliasNode, SourceNode } from './ModelMappingDiagramTypes'; +import styles from './ModelMappingDiagram.module.scss'; + +interface RenameAliasModalProps { + open: boolean; + t: TFunction; + value: string; + error: string; + onChange: (value: string) => void; + onClose: () => void; + onSubmit: () => void; +} + +export function RenameAliasModal({ + open, + t, + value, + error, + onChange, + onClose, + onSubmit +}: RenameAliasModalProps) { + return ( + + + + + } + > + onChange(e.target.value)} + onKeyDown={(e: KeyboardEvent) => { + if (e.key === 'Enter') onSubmit(); + }} + error={error} + placeholder={t('oauth_model_alias.diagram_rename_placeholder')} + autoFocus + /> + + ); +} + +interface AddAliasModalProps { + open: boolean; + t: TFunction; + value: string; + error: string; + onChange: (value: string) => void; + onClose: () => void; + onSubmit: () => void; +} + +export function AddAliasModal({ + open, + t, + value, + error, + onChange, + onClose, + onSubmit +}: AddAliasModalProps) { + return ( + + + + + } + > + onChange(e.target.value)} + onKeyDown={(e: KeyboardEvent) => { + if (e.key === 'Enter') onSubmit(); + }} + error={error} + placeholder={t('oauth_model_alias.diagram_add_placeholder')} + autoFocus + /> + + ); +} + +interface SettingsAliasModalProps { + open: boolean; + t: TFunction; + alias: string | null; + aliasNodes: AliasNode[]; + onClose: () => void; + onToggleFork: (provider: string, sourceModel: string, alias: string, fork: boolean) => void; + onUnlink: (provider: string, sourceModel: string, alias: string) => void; +} + +export function SettingsAliasModal({ + open, + t, + alias, + aliasNodes, + onClose, + onToggleFork, + onUnlink +}: SettingsAliasModalProps) { + return ( + + {t('common.close')} + + } + > + {alias ? ( + (() => { + const node = aliasNodes.find((n) => n.alias === alias); + if (!node || node.sources.length === 0) { + return
{t('oauth_model_alias.diagram_settings_empty')}
; + } + return ( +
+ {node.sources.map((source) => { + const entry = source.aliases.find((item) => item.alias === alias); + const forkEnabled = entry?.fork === true; + return ( +
+
+ {source.name} + + {alias} +
+
+ + {t('oauth_model_alias.alias_fork_label')} + + onToggleFork(source.provider, source.name, alias, value)} + ariaLabel={t('oauth_model_alias.alias_fork_label')} + /> + +
+
+ ); + })} +
+ ); + })() + ) : null} +
+ ); +} + +interface SettingsSourceModalProps { + open: boolean; + t: TFunction; + source: SourceNode | null; + onClose: () => void; + onToggleFork: (provider: string, sourceModel: string, alias: string, fork: boolean) => void; + onUnlink: (provider: string, sourceModel: string, alias: string) => void; +} + +export function SettingsSourceModal({ + open, + t, + source, + onClose, + onToggleFork, + onUnlink +}: SettingsSourceModalProps) { + return ( + + {t('common.close')} + + } + > + {source ? ( + source.aliases.length === 0 ? ( +
{t('oauth_model_alias.diagram_settings_empty')}
+ ) : ( +
+ {source.aliases.map((entry) => ( +
+
+ {source.name} + + {entry.alias} +
+
+ + {t('oauth_model_alias.alias_fork_label')} + + onToggleFork(source.provider, source.name, entry.alias, value)} + ariaLabel={t('oauth_model_alias.alias_fork_label')} + /> + +
+
+ ))} +
+ ) + ) : null} +
+ ); +} diff --git a/src/components/modelAlias/ModelMappingDiagramTypes.ts b/src/components/modelAlias/ModelMappingDiagramTypes.ts new file mode 100644 index 0000000..704692d --- /dev/null +++ b/src/components/modelAlias/ModelMappingDiagramTypes.ts @@ -0,0 +1,33 @@ +export interface AuthFileModelItem { + id: string; + display_name?: string; + type?: string; + owned_by?: string; +} + +export interface SourceNode { + id: string; // unique: provider::name + provider: string; + name: string; + aliases: { alias: string; fork: boolean }[]; // all aliases this source maps to +} + +export interface AliasNode { + id: string; // alias + alias: string; + sources: SourceNode[]; +} + +export interface ProviderNode { + provider: string; + sources: SourceNode[]; +} + +export interface ContextMenuState { + x: number; + y: number; + type: 'alias' | 'background' | 'provider' | 'source'; + data?: string; +} + +export type DiagramLine = { path: string; color: string; id: string };