From fb1192c3646e3c2ad83713085d609200dc34cb71 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 17:13:52 +0000 Subject: [PATCH 1/3] refactor(ui)!: retire plugin-designer's dead collaboration code, VersionHistory and the useConfirmDialog entry line The CollaborationProvider left the package entry in the previous batch and nothing mounts it, so useCollaboration() is always null: delete the module and the four designers' broadcast branches and status chips. VersionHistory has no reader in the package or outside it: delete the module, its test and its sub-barrel lines. useConfirmDialog stays module-local (seven designers use it); only the entry and README stop publishing it. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01QsR2jR9JS6qCZyzC5esvUr --- ...gner-dead-collaboration-version-history.md | 21 ++ ui/packages/plugin-designer/README.md | 5 +- ui/packages/plugin-designer/package.json | 2 +- .../src/CollaborationProvider.tsx | 252 ------------------ .../plugin-designer/src/DataModelDesigner.tsx | 22 +- .../plugin-designer/src/PageDesigner.tsx | 34 +-- .../plugin-designer/src/ProcessDesigner.tsx | 23 +- .../plugin-designer/src/ReportDesigner.tsx | 44 +-- ...VersionHistory.displayLocale-9909.test.tsx | 70 ----- .../ariaLabelNames.zh-12145.test.tsx | 12 +- .../src/components/VersionHistory.tsx | 126 --------- .../plugin-designer/src/components/index.ts | 3 - .../src/hooks/useDesignerTranslation.ts | 2 +- ui/packages/plugin-designer/src/index.tsx | 9 +- 14 files changed, 52 insertions(+), 573 deletions(-) create mode 100644 .changeset/25921-plugin-designer-dead-collaboration-version-history.md delete mode 100644 ui/packages/plugin-designer/src/CollaborationProvider.tsx delete mode 100644 ui/packages/plugin-designer/src/__tests__/VersionHistory.displayLocale-9909.test.tsx delete mode 100644 ui/packages/plugin-designer/src/components/VersionHistory.tsx diff --git a/.changeset/25921-plugin-designer-dead-collaboration-version-history.md b/.changeset/25921-plugin-designer-dead-collaboration-version-history.md new file mode 100644 index 00000000000..d9ecaee7ce7 --- /dev/null +++ b/.changeset/25921-plugin-designer-dead-collaboration-version-history.md @@ -0,0 +1,21 @@ +--- +'@object-ui/plugin-designer': patch +--- + +refactor(plugin-designer)!: retire `useConfirmDialog` and `VersionHistory` from the entry, and delete the designers' dead collaboration code + +Clause-②: yes +Metadata-protocol: none + + + +**BREAKING** for a host that imports `useConfirmDialog` or `VersionHistory` from `@object-ui/plugin-designer`. Neither had an importer anywhere in the workspace or in hotcrm, so the Console and every `@object-ui/*` package are unaffected. + +- The `VersionHistory` timeline module is deleted: nothing in the package rendered it. The `designer.versionHistory` translation key stays in the locale packs for now, with no reader. +- `useConfirmDialog` stays inside the package, because the designers use it for their delete prompts. Only the entry stops publishing it. +- The collaboration code that became unreachable once the previous release stopped exporting `CollaborationProvider` is deleted. That covers the provider module, the always-null `useCollaboration()` branches in the page, data model, process and report designers, and their "N users" / "Connected" toolbar chips. Nothing outside the package could mount the provider, so no designer behaves differently. + +What leaves, and what to write instead (neither has a replacement export): + +- FROM `useConfirmDialog` TO no replacement: render your own confirm prompt with `ConfirmDialog` (still exported) and local `useState`. +- FROM `VersionHistory` TO no replacement: render the version list in your own component. The designers never showed one. diff --git a/ui/packages/plugin-designer/README.md b/ui/packages/plugin-designer/README.md index 271a747d84f..6c7643778f4 100644 --- a/ui/packages/plugin-designer/README.md +++ b/ui/packages/plugin-designer/README.md @@ -125,18 +125,17 @@ declare const sections: ReportDesignerSection[]; ### Shared Hooks ```tsx -import { useUndoRedo, useConfirmDialog } from '@object-ui/plugin-designer'; +import { useUndoRedo } from '@object-ui/plugin-designer'; import type { DesignerComponent } from '@object-ui/types'; const { undo, redo, canUndo, canRedo } = useUndoRedo([]); -const { confirm, isOpen } = useConfirmDialog(); ``` ### Shared Components ```tsx -import { ConfirmDialog, Minimap, VersionHistory } from '@object-ui/plugin-designer'; +import { ConfirmDialog, Minimap } from '@object-ui/plugin-designer'; ``` ## Links diff --git a/ui/packages/plugin-designer/package.json b/ui/packages/plugin-designer/package.json index 00745533c3c..b79673ac9f1 100644 --- a/ui/packages/plugin-designer/package.json +++ b/ui/packages/plugin-designer/package.json @@ -3,7 +3,7 @@ "version": "18.2.0", "type": "module", "license": "MIT", - "description": "Visual designer plugin for ObjectUI with page, data model, process, and report designers plus collaborative editing.", + "description": "Visual designer plugin for ObjectUI with page, data model, process, and report designers.", "main": "dist/index.umd.cjs", "module": "dist/index.js", "types": "dist/index.d.ts", diff --git a/ui/packages/plugin-designer/src/CollaborationProvider.tsx b/ui/packages/plugin-designer/src/CollaborationProvider.tsx deleted file mode 100644 index 35f7f36fdc0..00000000000 --- a/ui/packages/plugin-designer/src/CollaborationProvider.tsx +++ /dev/null @@ -1,252 +0,0 @@ -/** - * ObjectUI - * Copyright (c) 2024-present ObjectStack Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -import React, { createContext, useContext, useCallback, useMemo, useEffect, useRef, useState } from 'react'; -import type { CollaborationConfig, CollaborationPresence, CollaborationOperation } from '@object-ui/types'; -import { clsx } from 'clsx'; -import { twMerge } from 'tailwind-merge'; - -function cn(...inputs: (string | undefined | false)[]) { - return twMerge(clsx(inputs)); -} - -export interface CollaborationContextValue { - /** Active users in the session */ - users: CollaborationPresence[]; - /** Whether collaboration is connected */ - isConnected: boolean; - /** Send an operation to collaborators */ - sendOperation: (operation: Omit) => void; - /** Current user ID */ - currentUserId?: string; - /** Connection state */ - connectionState: 'disconnected' | 'connecting' | 'connected' | 'error'; - /** Version history entries (when versionHistory is enabled) */ - versionCount: number; -} - -const CollabCtx = createContext(null); -CollabCtx.displayName = 'CollaborationContext'; - -export interface CollaborationProviderProps { - /** Collaboration configuration */ - config: CollaborationConfig; - /** Current user info */ - user?: { - id: string; - name: string; - avatar?: string; - }; - /** Callback when an operation is received from another user */ - onOperation?: (operation: CollaborationOperation) => void; - /** Callback when a remote user joins or leaves */ - onPresenceChange?: (users: CollaborationPresence[]) => void; - /** Children */ - children: React.ReactNode; -} - -/** - * Provider for multi-user collaborative editing. - * Manages WebSocket connection, presence, and operation broadcasting. - * Supports real-time collaboration via WebSocket when serverUrl is configured. - */ -export function CollaborationProvider({ - config, - user, - onOperation, - onPresenceChange, - children, -}: CollaborationProviderProps) { - const wsRef = useRef(null); - const [remoteUsers, setRemoteUsers] = useState([]); - const [connectionState, setConnectionState] = useState<'disconnected' | 'connecting' | 'connected' | 'error'>('disconnected'); - const versionRef = useRef(0); - const reconnectTimerRef = useRef | null>(null); - - // Connect via WebSocket when serverUrl is provided - useEffect(() => { - if (!config.enabled || !user || !config.serverUrl) return; - - const canUseWS = typeof WebSocket !== 'undefined'; - if (!canUseWS) return; - - function connect() { - // Validate WebSocket URL protocol (only ws: or wss: allowed) - let url: URL; - try { - url = new URL(config.serverUrl!); - if (url.protocol !== 'ws:' && url.protocol !== 'wss:') return; - } catch { - return; - } - if (config.roomId) url.searchParams.set('room', config.roomId); - url.searchParams.set('userId', user!.id); - - setConnectionState('connecting'); - const ws = new WebSocket(url.toString()); - - ws.onopen = () => { - setConnectionState('connected'); - // Send join message - ws.send(JSON.stringify({ - type: 'join', - userId: user!.id, - userName: user!.name, - avatar: user!.avatar, - })); - }; - - ws.onmessage = (event) => { - try { - const msg = JSON.parse(event.data); - if (msg.type === 'presence') { - const users = (msg.users || []) as CollaborationPresence[]; - setRemoteUsers(users.filter((u: CollaborationPresence) => u.userId !== user!.id)); - onPresenceChange?.(users); - } else if (msg.type === 'operation') { - onOperation?.(msg.operation as CollaborationOperation); - } - } catch { - // Ignore malformed messages - } - }; - - ws.onclose = () => { - setConnectionState('disconnected'); - wsRef.current = null; - // Auto-reconnect - if (config.enabled) { - reconnectTimerRef.current = setTimeout(connect, config.autoSaveInterval ?? 3000); - } - }; - - ws.onerror = () => { - setConnectionState('error'); - }; - - wsRef.current = ws; - } - - connect(); - - return () => { - if (reconnectTimerRef.current) clearTimeout(reconnectTimerRef.current); - if (wsRef.current) { - wsRef.current.onclose = null; // Prevent reconnect on intentional close - wsRef.current.close(); - wsRef.current = null; - } - setConnectionState('disconnected'); - }; - }, [config.enabled, config.serverUrl, config.roomId, config.autoSaveInterval, user, onOperation, onPresenceChange]); - - const users = useMemo(() => { - if (!config.enabled || !user) return []; - const currentUser: CollaborationPresence = { - userId: user.id, - userName: user.name, - avatar: user.avatar, - color: generateColor(user.id), - status: 'active' as const, - lastActivity: new Date().toISOString(), - }; - return [currentUser, ...remoteUsers]; - }, [config.enabled, user, remoteUsers]); - - const isConnected = config.enabled && !!user && (config.serverUrl ? connectionState === 'connected' : true); - - const sendOperation = useCallback( - (operation: Omit) => { - if (!config.enabled || !user) return; - - versionRef.current += 1; - const fullOp: CollaborationOperation = { - ...operation, - id: `op-${Date.now()}-${versionRef.current}`, - userId: user.id, - timestamp: new Date().toISOString(), - version: versionRef.current, - }; - - // Send via WebSocket if connected - if (wsRef.current?.readyState === WebSocket.OPEN) { - wsRef.current.send(JSON.stringify({ type: 'operation', operation: fullOp })); - } - - // Always notify local listeners - onOperation?.(fullOp); - }, - [config.enabled, user, onOperation], - ); - - const value = useMemo( - () => ({ - users, - isConnected, - sendOperation, - currentUserId: user?.id, - connectionState, - versionCount: versionRef.current, - }), - [users, isConnected, sendOperation, user?.id, connectionState], - ); - - return {children}; -} - -/** - * Hook to access the collaboration context. - */ -export function useCollaboration(): CollaborationContextValue | null { - return useContext(CollabCtx); -} - -/** - * Connection status indicator component. - * Shows the current collaboration connection state with a colored dot and label. - */ -export function ConnectionStatusIndicator({ className }: { className?: string }) { - const ctx = useContext(CollabCtx); - if (!ctx) return null; - - const { connectionState, users } = ctx; - const stateConfig: Record = { - connected: { color: 'bg-green-500', label: 'Connected' }, - connecting: { color: 'bg-yellow-500 animate-pulse', label: 'Connecting…' }, - disconnected: { color: 'bg-gray-400', label: 'Disconnected' }, - error: { color: 'bg-red-500', label: 'Connection error' }, - }; - const { color, label } = stateConfig[connectionState] ?? stateConfig.disconnected; - - return ( -
- - {label} - {connectionState === 'connected' && users.length > 1 && ( - ({users.length} users) - )} -
- ); -} - -/** - * Generate a consistent color for a user ID. - */ -function generateColor(userId: string): string { - const colors = [ - '#3b82f6', '#ef4444', '#22c55e', '#f59e0b', - '#8b5cf6', '#ec4899', '#06b6d4', '#f97316', - '#14b8a6', '#f43f5e', '#a855f7', '#84cc16', - '#0ea5e9', '#e879f9', '#fb923c', '#facc15', - ]; - let hash = 0; - for (let i = 0; i < userId.length; i++) { - hash = ((hash << 5) - hash + userId.charCodeAt(i)) | 0; - } - return colors[Math.abs(hash) % colors.length]; -} diff --git a/ui/packages/plugin-designer/src/DataModelDesigner.tsx b/ui/packages/plugin-designer/src/DataModelDesigner.tsx index 3ecdbba4d38..66dad1451e3 100644 --- a/ui/packages/plugin-designer/src/DataModelDesigner.tsx +++ b/ui/packages/plugin-designer/src/DataModelDesigner.tsx @@ -8,7 +8,7 @@ import React, { useState, useCallback, useEffect, useRef } from 'react'; import type { DataModelEntity, DataModelField, DataModelRelationship, DesignerCanvasConfig } from '@object-ui/types'; -import { Database, Plus, Trash2, Link2, Undo2, Redo2, Grid3X3, ZoomIn, ZoomOut, RotateCcw, ChevronDown, ChevronRight, Copy, Clipboard, Users } from 'lucide-react'; +import { Database, Plus, Trash2, Link2, Undo2, Redo2, Grid3X3, ZoomIn, ZoomOut, RotateCcw, ChevronDown, ChevronRight, Copy, Clipboard } from 'lucide-react'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@object-ui/components'; import { clsx } from 'clsx'; import { twMerge } from 'tailwind-merge'; @@ -20,7 +20,6 @@ import { useCanvasPanZoom } from './hooks/useCanvasPanZoom'; import { ConfirmDialog } from './components/ConfirmDialog'; import { PropertyEditor, type PropertyField } from './components/PropertyEditor'; import { Minimap } from './components/Minimap'; -import { useCollaboration } from './CollaborationProvider'; import { CANVAS_BACKGROUND_CLASS, canvasBackgroundStyle } from './canvasBackground'; function cn(...inputs: (string | undefined | false)[]) { @@ -253,7 +252,7 @@ export interface DataModelDesignerProps { * rows of its `sourceField` and `targetField`, with its `deleteBehavior` beside * the label (objectui#11434). * Supports undo/redo, multi-select, copy/paste, pan/zoom, drag-to-reposition, - * inline field editing, property editor, minimap, auto-layout, and collaboration. + * inline field editing, property editor, minimap and auto-layout. */ export function DataModelDesigner({ entities: initialEntities = [], @@ -300,9 +299,6 @@ export function DataModelDesigner({ // --- Pan/Zoom --- const panZoom = useCanvasPanZoom({ minZoom: 0.25, maxZoom: 3, zoomStep: 0.1 }); - // --- Collaboration --- - const collaboration = useCollaboration(); - // --- Collapsible panels --- const [showProperties, setShowProperties] = useState(true); const [showMinimap, setShowMinimap] = useState(true); @@ -343,8 +339,7 @@ export function DataModelDesigner({ }; pushState(next); multiSelect.selectOne(newEntity.id); - collaboration?.sendOperation({ type: 'insert', userId: collaboration?.currentUserId ?? '', elementId: newEntity.id, data: { name: newEntity.name, label: newEntity.label } }); - }, [readOnly, undoRedo, pushState, multiSelect, collaboration]); + }, [readOnly, undoRedo, pushState, multiSelect]); const handleDeleteEntity = useCallback( async (id: string) => { @@ -367,9 +362,8 @@ export function DataModelDesigner({ if (multiSelect.isSelected(id)) { multiSelect.clearSelection(); } - collaboration?.sendOperation({ type: 'delete', userId: collaboration?.currentUserId ?? '', elementId: id, data: {} }); }, - [readOnly, entities, confirmDialog, undoRedo, pushState, multiSelect, collaboration], + [readOnly, entities, confirmDialog, undoRedo, pushState, multiSelect], ); const handleDeleteSelected = useCallback(async () => { @@ -788,14 +782,6 @@ export function DataModelDesigner({
- {/* Collaboration status */} - {collaboration?.isConnected && ( -
- - {collaboration.users.length} user{collaboration.users.length !== 1 ? 's' : ''} -
- )} - {/* Multi-select count */} {multiSelect.count > 1 && ( {multiSelect.count} selected diff --git a/ui/packages/plugin-designer/src/PageDesigner.tsx b/ui/packages/plugin-designer/src/PageDesigner.tsx index 1e261fc31bd..bfa14612bcf 100644 --- a/ui/packages/plugin-designer/src/PageDesigner.tsx +++ b/ui/packages/plugin-designer/src/PageDesigner.tsx @@ -42,7 +42,6 @@ import { useCanvasPanZoom } from './hooks/useCanvasPanZoom'; import { ConfirmDialog } from './components/ConfirmDialog'; import { PropertyEditor, type PropertyField } from './components/PropertyEditor'; import { Minimap, type MinimapItem } from './components/Minimap'; -import { useCollaboration } from './CollaborationProvider'; import { CANVAS_BACKGROUND_CLASS, canvasBackgroundStyle } from './canvasBackground'; function cn(...inputs: (string | undefined | false)[]) { @@ -165,17 +164,6 @@ export function PageDesigner({ // -- Confirm dialog ------------------------------------------------------- const confirmDialog = useConfirmDialog(); - // -- Collaboration (optional) --------------------------------------------- - const collaboration = useCollaboration(); - - const broadcastOp = useCallback( - (op: { type: 'insert' | 'update' | 'delete' | 'move' | 'resize'; elementId: string; data: Record }) => { - if (!collaboration) return; - collaboration.sendOperation({ ...op, userId: collaboration.currentUserId ?? '' }); - }, - [collaboration], - ); - // -- Collapsible panels --------------------------------------------------- const [leftPanelOpen, setLeftPanelOpen] = useState(true); const [rightPanelOpen, setRightPanelOpen] = useState(true); @@ -218,9 +206,8 @@ export function PageDesigner({ const updated = [...components, newComponent]; pushComponents(updated); multiSelect.selectOne(newComponent.id); - broadcastOp({ type: 'insert', elementId: newComponent.id, data: { component: newComponent } }); }, - [components, readOnly, palette, pushComponents, multiSelect, broadcastOp], + [components, readOnly, palette, pushComponents, multiSelect], ); const handleDeleteComponent = useCallback( @@ -231,9 +218,8 @@ export function PageDesigner({ const updated = filterTree(components, (c) => c.id !== id); pushComponents(updated); if (multiSelect.isSelected(id)) multiSelect.clearSelection(); - broadcastOp({ type: 'delete', elementId: id, data: {} }); }, - [components, readOnly, pushComponents, multiSelect, broadcastOp], + [components, readOnly, pushComponents, multiSelect], ); const handleDeleteSelected = useCallback(async () => { @@ -255,10 +241,7 @@ export function PageDesigner({ const updated = filterTree(components, (c) => !ids.has(c.id)); pushComponents(updated); multiSelect.clearSelection(); - ids.forEach((id) => { - broadcastOp({ type: 'delete', elementId: id, data: {} }); - }); - }, [readOnly, multiSelect, confirmDialog, components, pushComponents, broadcastOp]); + }, [readOnly, multiSelect, confirmDialog, components, pushComponents]); const handleUpdateProperty = useCallback( (name: string, value: unknown) => { @@ -275,9 +258,8 @@ export function PageDesigner({ return { ...c, props: { ...c.props, [name]: value } }; }); pushComponents(updated); - broadcastOp({ type: 'update', elementId: selectedId, data: { [name]: value } }); }, - [readOnly, selectedId, components, pushComponents, broadcastOp], + [readOnly, selectedId, components, pushComponents], ); // -- Copy / Paste --------------------------------------------------------- @@ -303,10 +285,7 @@ export function PageDesigner({ const updated = [...components, ...pasted]; pushComponents(updated); multiSelect.selectMany(pasted.map((p) => p.id)); - pasted.forEach((p) => { - broadcastOp({ type: 'insert', elementId: p.id, data: { component: p } }); - }); - }, [readOnly, clipboard, components, pushComponents, multiSelect, broadcastOp]); + }, [readOnly, clipboard, components, pushComponents, multiSelect]); // -- Drag-and-drop handlers ----------------------------------------------- const handleDragStart = useCallback( @@ -343,10 +322,9 @@ export function PageDesigner({ }; }); pushComponents(updated); - broadcastOp({ type: 'move', elementId: drag.id, data: {} }); dragRef.current = null; }, - [readOnly, zoom, components, pushComponents, snapToGrid, broadcastOp], + [readOnly, zoom, components, pushComponents, snapToGrid], ); // -- Property fields for PropertyEditor ----------------------------------- diff --git a/ui/packages/plugin-designer/src/ProcessDesigner.tsx b/ui/packages/plugin-designer/src/ProcessDesigner.tsx index 17cba14020a..a0f0e5f0765 100644 --- a/ui/packages/plugin-designer/src/ProcessDesigner.tsx +++ b/ui/packages/plugin-designer/src/ProcessDesigner.tsx @@ -23,7 +23,6 @@ import { useCanvasPanZoom } from './hooks/useCanvasPanZoom'; import { ConfirmDialog } from './components/ConfirmDialog'; import { PropertyEditor, type PropertyField } from './components/PropertyEditor'; import { Minimap } from './components/Minimap'; -import { useCollaboration } from './CollaborationProvider'; import { CANVAS_BACKGROUND_CLASS, canvasBackgroundStyle } from './canvasBackground'; function cn(...inputs: (string | undefined | false)[]) { @@ -216,7 +215,7 @@ function autoLayoutNodes(nodes: BPMNNode[], edges: BPMNEdge[]): BPMNNode[] { * * Phase 2-4 features: undo/redo, confirm dialogs, multi-select, * copy/paste, pan/zoom, minimap, edge creation UI, auto-layout, - * property editor, drag-and-drop, collaboration, keyboard shortcuts, + * property editor, drag-and-drop, keyboard shortcuts, * and collapsible panels. */ export function ProcessDesigner({ @@ -262,9 +261,6 @@ export function ProcessDesigner({ // ---- Pan / Zoom ---- const panZoom = useCanvasPanZoom({ initialZoom: 1 }); - // ---- Collaboration (optional context) ---- - const collaboration = useCollaboration(); - // ---- Local UI state ---- const [selectedNodeId, setSelectedNodeId] = useState(null); const [connectMode, setConnectMode] = useState(false); @@ -300,9 +296,8 @@ export function ProcessDesigner({ pushSnapshot(updatedNodes, edges); setSelectedNodeId(newNode.id); multiSelect.selectOne(newNode.id); - collaboration?.sendOperation({ type: 'insert', elementId: newNode.id, data: { label }, userId: collaboration.currentUserId ?? '' }); }, - [nodes, edges, readOnly, pushSnapshot, multiSelect, collaboration], + [nodes, edges, readOnly, pushSnapshot, multiSelect], ); // ---- Delete node (with confirmation) ---- @@ -321,9 +316,8 @@ export function ProcessDesigner({ pushSnapshot(updatedNodes, updatedEdges); if (selectedNodeId === id) setSelectedNodeId(null); multiSelect.clearSelection(); - collaboration?.sendOperation({ type: 'delete', elementId: id, data: {}, userId: collaboration.currentUserId ?? '' }); }, - [nodes, edges, selectedNodeId, readOnly, pushSnapshot, confirmDialog, multiSelect, collaboration], + [nodes, edges, selectedNodeId, readOnly, pushSnapshot, confirmDialog, multiSelect], ); // ---- Delete multiple selected nodes ---- @@ -397,10 +391,9 @@ export function ProcessDesigner({ pushSnapshot(nodes, updatedEdges); setConnectSource(null); setConnectMode(false); - collaboration?.sendOperation({ type: 'insert', elementId: newEdge.id, data: { source: connectSource, target: nodeId }, userId: collaboration.currentUserId ?? '' }); return true; }, - [connectMode, connectSource, nodes, edges, pushSnapshot, collaboration], + [connectMode, connectSource, nodes, edges, pushSnapshot], ); // ---- Auto layout ---- @@ -669,14 +662,6 @@ export function ProcessDesigner({ )} - {/* Collaboration indicator */} - {collaboration?.isConnected && ( -
- - {collaboration.users.length} user{collaboration.users.length !== 1 ? 's' : ''} -
- )} -
{!readOnly && ( diff --git a/ui/packages/plugin-designer/src/ReportDesigner.tsx b/ui/packages/plugin-designer/src/ReportDesigner.tsx index e4676f8dcfc..6b93a98846a 100644 --- a/ui/packages/plugin-designer/src/ReportDesigner.tsx +++ b/ui/packages/plugin-designer/src/ReportDesigner.tsx @@ -30,7 +30,6 @@ import { useMultiSelect } from './hooks/useMultiSelect'; import { useClipboard } from './hooks/useClipboard'; import { ConfirmDialog } from './components/ConfirmDialog'; import { PropertyEditor, type PropertyField } from './components/PropertyEditor'; -import { useCollaboration } from './CollaborationProvider'; function cn(...inputs: (string | undefined | false)[]) { return twMerge(clsx(inputs)); @@ -67,7 +66,6 @@ const LABELS = { `Are you sure you want to delete ${count} element${count > 1 ? 's' : ''}?`, collapsePanel: 'Collapse panel', expandPanel: 'Expand panel', - collaborationConnected: 'Connected', sectionHeader: 'Report Header', sectionPageHeader: 'Page Header', sectionGroupHeader: 'Group Header', @@ -244,17 +242,6 @@ export function ReportDesigner({ // -- Confirm dialog ------------------------------------------------------- const confirmDialog = useConfirmDialog(); - // -- Collaboration (optional) --------------------------------------------- - const collaboration = useCollaboration(); - - const broadcastOp = useCallback( - (op: { type: 'insert' | 'update' | 'delete' | 'move'; elementId: string; data: Record }) => { - if (!collaboration) return; - collaboration.sendOperation({ ...op, userId: collaboration.currentUserId ?? '' }); - }, - [collaboration], - ); - // -- Collapsible property panel ------------------------------------------- const [propertyPanelOpen, setPropertyPanelOpen] = useState(true); @@ -314,9 +301,8 @@ export function ReportDesigner({ ); pushSections(updated); multiSelect.selectOne(newElement.id); - broadcastOp({ type: 'insert', elementId: newElement.id, data: { element: newElement } }); }, - [sections, margins.left, readOnly, pushSections, multiSelect, broadcastOp], + [sections, margins.left, readOnly, pushSections, multiSelect], ); // -- Delete single element (no confirm) ----------------------------------- @@ -329,9 +315,8 @@ export function ReportDesigner({ })); pushSections(updated); if (multiSelect.isSelected(elementId)) multiSelect.clearSelection(); - broadcastOp({ type: 'delete', elementId, data: {} }); }, - [sections, readOnly, pushSections, multiSelect, broadcastOp], + [sections, readOnly, pushSections, multiSelect], ); // -- Delete selected with confirmation ------------------------------------ @@ -349,10 +334,7 @@ export function ReportDesigner({ })); pushSections(updated); multiSelect.clearSelection(); - ids.forEach((id) => { - broadcastOp({ type: 'delete', elementId: id, data: {} }); - }); - }, [readOnly, multiSelect, confirmDialog, sections, pushSections, broadcastOp]); + }, [readOnly, multiSelect, confirmDialog, sections, pushSections]); // -- Update element property ---------------------------------------------- const handleUpdateProperty = useCallback( @@ -378,9 +360,8 @@ export function ReportDesigner({ }), })); pushSections(updated); - broadcastOp({ type: 'update', elementId: selectedId, data: { [name]: value } }); }, - [readOnly, selectedId, sections, pushSections, broadcastOp], + [readOnly, selectedId, sections, pushSections], ); // -- Copy / Paste --------------------------------------------------------- @@ -412,10 +393,7 @@ export function ReportDesigner({ ); pushSections(updated); multiSelect.selectMany(pasted.map((p) => p.id)); - pasted.forEach((p) => { - broadcastOp({ type: 'insert', elementId: p.id, data: { element: p } }); - }); - }, [readOnly, clipboard, sections, pushSections, multiSelect, broadcastOp]); + }, [readOnly, clipboard, sections, pushSections, multiSelect]); // -- Drag-and-drop handlers ----------------------------------------------- const handleDragStart = useCallback( @@ -490,10 +468,9 @@ export function ReportDesigner({ pushSections(updated); } } - broadcastOp({ type: 'move', elementId: drag.id, data: {} }); dragRef.current = null; }, - [readOnly, sections, pushSections, broadcastOp], + [readOnly, sections, pushSections], ); // -- Property fields for PropertyEditor ----------------------------------- @@ -661,15 +638,6 @@ export function ReportDesigner({
- {/* Collaboration indicator */} - {collaboration?.isConnected && ( - - - {LABELS.collaborationConnected} - {collaboration.users.length > 1 && ` (${collaboration.users.length})`} - - )} - {pageSize} {orientation} diff --git a/ui/packages/plugin-designer/src/__tests__/VersionHistory.displayLocale-9909.test.tsx b/ui/packages/plugin-designer/src/__tests__/VersionHistory.displayLocale-9909.test.tsx deleted file mode 100644 index b36ecddc60a..00000000000 --- a/ui/packages/plugin-designer/src/__tests__/VersionHistory.displayLocale-9909.test.tsx +++ /dev/null @@ -1,70 +0,0 @@ -/** - * ObjectUI - * Copyright (c) 2024-present ObjectStack Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -/** - * `VersionHistory`'s version times read the DECLARED session locale, never the - * machine's (objectui#9909). - * - * The same entries render under a declared `de-DE` tenant locale and a - * declared `en` one — the UI language `en` on both — and must read - * differently: a literal would measure the runner, on which a broken surface - * and a repaired one print the same bytes. The runtime tripwire then checks - * the argument every locale-taking call received. - */ - -import * as React from 'react'; -import { describe, it, expect, afterEach } from 'vitest'; -import { render, cleanup } from '@testing-library/react'; -import { I18nProvider, LocalizationProvider } from '@object-ui/i18n'; -import { isMachineLocale, recordLocaleArguments } from '@object-ui/test-support'; -import { VersionHistory } from '../components/VersionHistory'; - -afterEach(() => cleanup()); - -function session(locale: string, node: React.ReactNode): React.ReactElement { - return ( - - {node} - - ); -} - -const VERSIONS = [ - { version: 2, timestamp: '2020-03-04T15:30:00.000Z', userId: 'u1', userName: 'Ada', description: 'Renamed', isCurrent: true }, -]; - -function textUnder(locale: string): string { - render(session(locale, )); - const text = document.body.textContent ?? ''; - cleanup(); - return text.replace(/\s+/g, ' ').trim(); -} - -describe('VersionHistory times follow the declared session locale (objectui#9909)', () => { - it('says the de-DE face under a de-DE session', () => { - expect(textUnder('de-DE')).toMatch(/4\.3\.2020, 15:30:00/); - }); - - it('keeps its en face under an en session', () => { - expect(textUnder('en')).toMatch(/3\/4\/2020, 3:30:00\sPM/); - }); - - /** ⭐ THE PIN: the runner's own locale cannot satisfy it. */ - it('is a reading of the session, not of the machine', () => { - expect(textUnder('de-DE')).not.toBe(textUnder('en')); - }); - - it('every locale-taking call receives the declared tag', () => { - const calls = recordLocaleArguments(() => { - render(session('de-DE', )); - }); - cleanup(); - expect(calls.some((c) => c.locale === 'de-DE'), `saw: ${JSON.stringify(calls)}`).toBe(true); - expect(calls.filter(isMachineLocale)).toEqual([]); - }); -}); diff --git a/ui/packages/plugin-designer/src/__tests__/ariaLabelNames.zh-12145.test.tsx b/ui/packages/plugin-designer/src/__tests__/ariaLabelNames.zh-12145.test.tsx index dca0d0111f9..ef27df076b5 100644 --- a/ui/packages/plugin-designer/src/__tests__/ariaLabelNames.zh-12145.test.tsx +++ b/ui/packages/plugin-designer/src/__tests__/ariaLabelNames.zh-12145.test.tsx @@ -12,8 +12,7 @@ * hears the zh pack's words: * * - `HistoryPanel`'s undo / redo buttons: `designer.undo` / `designer.redo`; - * - `Minimap`'s image: `designer.canvasMinimap`; - * - `VersionHistory`'s region: `designer.versionHistory`. + * - `Minimap`'s image: `designer.canvasMinimap`. * * (`DashboardDesignPage`'s back button reads `detail.goBack` through the same * hook.) Every name is read against the PACK, never against a literal. @@ -26,7 +25,6 @@ import { I18nProvider } from '@object-ui/i18n'; import { en, zh } from '@object-ui/i18n/locales'; import { HistoryPanel } from '../components/HistoryPanel'; import { Minimap } from '../components/Minimap'; -import { VersionHistory } from '../components/VersionHistory'; import type { UndoRedoState } from '../hooks/useUndoRedo'; afterEach(cleanup); @@ -52,17 +50,15 @@ const designerChrome = () => ( <> - ); -type Names = { undo: string; redo: string; canvasMinimap: string; versionHistory: string }; +type Names = { undo: string; redo: string; canvasMinimap: string }; function expectNamed(names: Names) { expect(screen.getByRole('button', { name: names.undo })).toBeInTheDocument(); expect(screen.getByRole('button', { name: names.redo })).toBeInTheDocument(); expect(screen.getByRole('img', { name: names.canvasMinimap })).toBeInTheDocument(); - expect(screen.getByRole('region', { name: names.versionHistory })).toBeInTheDocument(); } describe('designer chrome is named from the pack (objectui#12145)', () => { @@ -72,7 +68,7 @@ describe('designer chrome is named from the pack (objectui#12145)', () => { }); it('under zh, each is named by the zh pack, not by the English words', async () => { - for (const k of ['undo', 'redo', 'canvasMinimap', 'versionHistory'] as const) { + for (const k of ['undo', 'redo', 'canvasMinimap'] as const) { expect(zh.designer[k]).not.toBe(en.designer[k]); } render( @@ -80,7 +76,7 @@ describe('designer chrome is named from the pack (objectui#12145)', () => { {designerChrome()} , ); - await screen.findByRole('region', { name: zh.designer.versionHistory }); + await screen.findByRole('img', { name: zh.designer.canvasMinimap }); expectNamed(zh.designer); expect(screen.queryByRole('button', { name: en.designer.undo })).toBeNull(); }); diff --git a/ui/packages/plugin-designer/src/components/VersionHistory.tsx b/ui/packages/plugin-designer/src/components/VersionHistory.tsx deleted file mode 100644 index 69843855152..00000000000 --- a/ui/packages/plugin-designer/src/components/VersionHistory.tsx +++ /dev/null @@ -1,126 +0,0 @@ -/** - * ObjectUI - * Copyright (c) 2024-present ObjectStack Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -import React from 'react'; -import { clsx } from 'clsx'; -import { twMerge } from 'tailwind-merge'; -import { History, User, Clock } from 'lucide-react'; -import { useDisplayLocale } from '@object-ui/i18n'; -import { useDesignerTranslation } from '../hooks/useDesignerTranslation'; - -function cn(...inputs: (string | undefined | false)[]) { - return twMerge(clsx(inputs)); -} - -/** - * One row of the {@link VersionHistory} timeline. - * - * Named `VersionHistoryEntry`, not `VersionEntry` (objectui#6349, batch 10). - * `@object-ui/collaboration` publishes a `VersionEntry` with a different - * meaning: a version its `useConflictResolution` hook RECORDS, with an `id` - * and a per-field `changes` diff that `revertToVersion` and `compareVersions` - * read. This is a display row instead: a prose `description` and an - * `isCurrent` flag, with no diff. This package's entry never exported the - * old name, so the rename moves no import. - */ -export interface VersionHistoryEntry { - /** Version number */ - version: number; - /** Timestamp */ - timestamp: string; - /** User who made the change */ - userId: string; - /** User display name */ - userName: string; - /** Description of changes */ - description: string; - /** Whether this is the current version */ - isCurrent?: boolean; -} - -export interface VersionHistoryProps { - /** Version entries */ - versions: VersionHistoryEntry[]; - /** Called when user wants to restore a version */ - onRestore?: (version: number) => void; - /** CSS class */ - className?: string; -} - -/** - * Version history browser with visual diff support. - * Shows a timeline of changes with the ability to restore previous versions. - */ -export function VersionHistory({ - versions, - onRestore, - className, -}: VersionHistoryProps) { - // Each version's time is formatted in the display locale — a bare - // `toLocaleString()` used the MACHINE's locale (objectui#9909). - const displayLocale = useDisplayLocale(); - const { t } = useDesignerTranslation(); - return ( -
-
- - Version History -
-
- {versions.length === 0 ? ( -
- No version history available -
- ) : ( -
- {/* Timeline line */} -
- {versions.map((entry) => ( -
- {/* Timeline dot */} -
-
-
- v{entry.version} - {entry.isCurrent && ( - Current - )} -
-

{entry.description}

-
- - {entry.userName} - - {new Date(entry.timestamp).toLocaleString(displayLocale)} -
- {!entry.isCurrent && onRestore && ( - - )} -
-
- ))} -
- )} -
-
- ); -} diff --git a/ui/packages/plugin-designer/src/components/index.ts b/ui/packages/plugin-designer/src/components/index.ts index 8bf719f39bc..c0b2d5aff88 100644 --- a/ui/packages/plugin-designer/src/components/index.ts +++ b/ui/packages/plugin-designer/src/components/index.ts @@ -15,8 +15,5 @@ export type { MinimapProps, MinimapItem } from './Minimap'; export { PropertyEditor } from './PropertyEditor'; export type { PropertyEditorProps, PropertyField } from './PropertyEditor'; -export { VersionHistory } from './VersionHistory'; -export type { VersionHistoryProps, VersionHistoryEntry } from './VersionHistory'; - export { HistoryPanel } from './HistoryPanel'; export type { HistoryPanelProps } from './HistoryPanel'; diff --git a/ui/packages/plugin-designer/src/hooks/useDesignerTranslation.ts b/ui/packages/plugin-designer/src/hooks/useDesignerTranslation.ts index dc96e0b435f..b92781321ad 100644 --- a/ui/packages/plugin-designer/src/hooks/useDesignerTranslation.ts +++ b/ui/packages/plugin-designer/src/hooks/useDesignerTranslation.ts @@ -212,7 +212,7 @@ export const DESIGNER_DEFAULT_TRANSLATIONS: Record = { // here beside the other borrowed `common.*` entries — not in `packages/components`. 'common.delete': 'Delete', // objectui#12145 — accessible names: `HistoryPanel`'s undo / redo buttons, - // `Minimap`'s image, `VersionHistory`'s region, and `DashboardDesignPage`'s + // `Minimap`'s image, and `DashboardDesignPage`'s // back button. The back button borrows `detail.goBack` rather than adding a // second key for the same name. 'designer.undo': 'Undo', diff --git a/ui/packages/plugin-designer/src/index.tsx b/ui/packages/plugin-designer/src/index.tsx index d5b79bfd613..60c4b0a393c 100644 --- a/ui/packages/plugin-designer/src/index.tsx +++ b/ui/packages/plugin-designer/src/index.tsx @@ -41,13 +41,11 @@ export type { BrandingEditorProps } from './BrandingEditor'; export type { ObjectManagerProps } from './ObjectManager'; export type { FieldDesignerProps } from './FieldDesigner'; -// Shared hooks. The clipboard, multi-select and pan/zoom hooks, the -// `PropertyEditor` panel and the collaboration provider with its status -// indicator are the designers' internals: nothing outside this package read -// them, so they stay module-local. +// Shared hooks. The clipboard, multi-select, pan/zoom and confirm-dialog hooks +// and the `PropertyEditor` panel are the designers' internals: nothing outside +// this package read them, so they stay module-local. export { useUndoRedo } from './hooks/useUndoRedo'; export { useDesignerHistory } from './hooks/useDesignerHistory'; -export { useConfirmDialog } from './hooks/useConfirmDialog'; // The provider-less fallback table, exported so objectui#4401's mirror gate can // compare it against the `en` pack from a package that depends on both. export { DESIGNER_DEFAULT_TRANSLATIONS } from './hooks/useDesignerTranslation'; @@ -55,7 +53,6 @@ export { DESIGNER_DEFAULT_TRANSLATIONS } from './hooks/useDesignerTranslation'; // Shared components export { ConfirmDialog } from './components/ConfirmDialog'; export { Minimap } from './components/Minimap'; -export { VersionHistory } from './components/VersionHistory'; // Route-ready app authoring pages — host apps mount these at their // preferred routes. Each page expects an active app/adapter context from From 681e6c7a6b6482c23b76bfb26b132e43777801c4 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 18:11:50 +0000 Subject: [PATCH 2/3] test(i18n): drop the two retired designer "Collaboration active" rows from the aria-label census The data model and process designers no longer render the collaboration status chip, so the census declares one literal fewer in each file. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01QsR2jR9JS6qCZyzC5esvUr --- .../i18n/src/__tests__/ariaLabelLiteralCensus-12145.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/packages/i18n/src/__tests__/ariaLabelLiteralCensus-12145.test.ts b/ui/packages/i18n/src/__tests__/ariaLabelLiteralCensus-12145.test.ts index 9177e77b588..1a42436ff76 100644 --- a/ui/packages/i18n/src/__tests__/ariaLabelLiteralCensus-12145.test.ts +++ b/ui/packages/i18n/src/__tests__/ariaLabelLiteralCensus-12145.test.ts @@ -252,7 +252,7 @@ const BACKLOG: Declared[] = ( 'packages/plugin-designer/src/DataModelDesigner.tsx', [ 'Designer toolbar', 'Undo', 'Redo', 'Zoom Out', 'Zoom In', 'Reset Zoom', 'Copy', 'Paste', 'Auto Layout', - 'Collaboration active', 'Add Entity', 'Add Relationship', 'Canvas', + 'Add Entity', 'Add Relationship', 'Canvas', ], ], ['packages/plugin-designer/src/EditorModeToggle.tsx', ['Editor mode']], @@ -267,7 +267,7 @@ const BACKLOG: Declared[] = ( [ 'packages/plugin-designer/src/ProcessDesigner.tsx', [ - 'Process toolbar', 'Process name', 'Process version', 'Collaboration active', 'Undo', 'Redo', 'Copy', + 'Process toolbar', 'Process name', 'Process version', 'Undo', 'Redo', 'Copy', 'Paste', 'Connect nodes', 'Auto layout', 'Zoom in', 'Zoom level', 'Zoom out', 'Reset zoom', 'Add Start Event', 'Add User Task', 'Add Gateway', 'Add End Event', 'Process canvas', 'Delete node', 'Property panel', From bf9140860e74a6e8fc65f15a49f7ec370656bea2 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 18:34:17 +0000 Subject: [PATCH 3/3] chore(changeset): declare the plugin-designer entry retirement as a narrowing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The batch only removes names from the package entry, so the Clause-② line reads "no (narrowing)" rather than a bare "yes". Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01QsR2jR9JS6qCZyzC5esvUr --- .../25921-plugin-designer-dead-collaboration-version-history.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/25921-plugin-designer-dead-collaboration-version-history.md b/.changeset/25921-plugin-designer-dead-collaboration-version-history.md index d9ecaee7ce7..ed8af333630 100644 --- a/.changeset/25921-plugin-designer-dead-collaboration-version-history.md +++ b/.changeset/25921-plugin-designer-dead-collaboration-version-history.md @@ -4,7 +4,7 @@ refactor(plugin-designer)!: retire `useConfirmDialog` and `VersionHistory` from the entry, and delete the designers' dead collaboration code -Clause-②: yes +Clause-②: no (narrowing) Metadata-protocol: none