diff --git a/web/app/components/workflow/collaboration/core/collaboration-manager.ts b/web/app/components/workflow/collaboration/core/collaboration-manager.ts index 1cb8a8afe8..fd784ea57c 100644 --- a/web/app/components/workflow/collaboration/core/collaboration-manager.ts +++ b/web/app/components/workflow/collaboration/core/collaboration-manager.ts @@ -35,6 +35,7 @@ export class CollaborationManager { private nodePanelPresence: NodePanelPresenceMap = {} private activeConnections = new Set() private isUndoRedoInProgress = false + private pendingInitialSync = false private getNodePanelPresenceSnapshot(): NodePanelPresenceMap { const snapshot: NodePanelPresenceMap = {} @@ -741,6 +742,8 @@ export class CollaborationManager { .map(node => node.id), ) + this.pendingInitialSync = false + const updatedNodes = Array .from(this.nodesMap.values()) .map((node: Node) => { @@ -790,6 +793,8 @@ export class CollaborationManager { const updatedEdges = Array.from(this.edgesMap.values()) console.log('Updating React edges from subscription') + this.pendingInitialSync = false + // Call ReactFlow's native setter directly to avoid triggering collaboration state.setEdges(updatedEdges) }) @@ -852,6 +857,11 @@ export class CollaborationManager { this.eventEmitter.emit('syncRequest', {}) } } + else if (update.type === 'graph_resync_request') { + console.log('Received graph resync request from collaborator') + if (this.isLeader) + this.broadcastCurrentGraph() + } }) socket.on('online_users', (data: { users: OnlineUser[]; leader?: string }) => { @@ -898,6 +908,11 @@ export class CollaborationManager { const wasLeader = this.isLeader this.isLeader = data.isLeader + if (this.isLeader) + this.pendingInitialSync = false + else + this.requestInitialSyncIfNeeded() + if (wasLeader !== this.isLeader) this.eventEmitter.emit('leaderChange', this.isLeader) } @@ -912,6 +927,10 @@ export class CollaborationManager { console.log(`Collaboration: I am now the ${this.isLeader ? 'Leader' : 'Follower'}.`) this.eventEmitter.emit('leaderChange', this.isLeader) } + if (this.isLeader) + this.pendingInitialSync = false + else + this.requestInitialSyncIfNeeded() }) socket.on('status', (data: { isLeader: boolean }) => { @@ -920,11 +939,16 @@ export class CollaborationManager { console.log(`Collaboration: I am now the ${this.isLeader ? 'Leader' : 'Follower'}.`) this.eventEmitter.emit('leaderChange', this.isLeader) } + if (this.isLeader) + this.pendingInitialSync = false + else + this.requestInitialSyncIfNeeded() }) socket.on('connect', () => { console.log('WebSocket connected successfully') this.eventEmitter.emit('stateChange', { isConnected: true }) + this.pendingInitialSync = true }) socket.on('disconnect', (reason: string) => { @@ -932,6 +956,7 @@ export class CollaborationManager { this.cursors = {} this.isLeader = false this.leaderId = null + this.pendingInitialSync = false this.eventEmitter.emit('stateChange', { isConnected: false }) this.eventEmitter.emit('cursors', {}) }) @@ -945,6 +970,49 @@ export class CollaborationManager { console.error('WebSocket error:', error) }) } + + // We currently only relay CRDT updates; the server doesn't persist them. + // When a follower joins mid-session, it might miss earlier broadcasts and render stale data. + // This lightweight checkpoint asks the leader to rebroadcast the latest graph snapshot once. + private requestInitialSyncIfNeeded(): void { + if (!this.pendingInitialSync) return + if (this.isLeader) { + this.pendingInitialSync = false + return + } + + this.emitGraphResyncRequest() + this.pendingInitialSync = false + } + + private emitGraphResyncRequest(): void { + if (!this.currentAppId || !webSocketClient.isConnected(this.currentAppId)) return + + const socket = webSocketClient.getSocket(this.currentAppId) + if (!socket) return + + socket.emit('collaboration_event', { + type: 'graph_resync_request', + data: { timestamp: Date.now() }, + timestamp: Date.now(), + }) + } + + private broadcastCurrentGraph(): void { + if (!this.currentAppId || !webSocketClient.isConnected(this.currentAppId)) return + if (!this.doc) return + + const socket = webSocketClient.getSocket(this.currentAppId) + if (!socket) return + + try { + const snapshot = this.doc.export({ mode: 'snapshot' }) + socket.emit('graph_event', snapshot) + } + catch (error) { + console.error('Failed to broadcast graph snapshot:', error) + } + } } export const collaborationManager = new CollaborationManager() diff --git a/web/app/components/workflow/hooks/use-workflow-comment.ts b/web/app/components/workflow/hooks/use-workflow-comment.ts index 4c2ea7548b..217fa6afb0 100644 --- a/web/app/components/workflow/hooks/use-workflow-comment.ts +++ b/web/app/components/workflow/hooks/use-workflow-comment.ts @@ -6,13 +6,13 @@ import { ControlMode } from '../types' import type { WorkflowCommentDetail, WorkflowCommentList } from '@/service/workflow-comment' import { createWorkflowComment, createWorkflowCommentReply, deleteWorkflowComment, deleteWorkflowCommentReply, fetchWorkflowComment, fetchWorkflowComments, resolveWorkflowComment, updateWorkflowComment, updateWorkflowCommentReply } from '@/service/workflow-comment' import { collaborationManager } from '@/app/components/workflow/collaboration' +import { useAppContext } from '@/context/app-context' export const useWorkflowComment = () => { const params = useParams() const appId = params.appId as string const reactflow = useReactFlow() const controlMode = useStore(s => s.controlMode) - const setControlMode = useStore(s => s.setControlMode) const pendingComment = useStore(s => s.pendingComment) const setPendingComment = useStore(s => s.setPendingComment) const setActiveCommentId = useStore(s => s.setActiveCommentId) @@ -31,6 +31,9 @@ export const useWorkflowComment = () => { const setReplyUpdating = useStore(s => s.setReplyUpdating) const commentDetailCache = useStore(s => s.commentDetailCache) const setCommentDetailCache = useStore(s => s.setCommentDetailCache) + const rightPanelWidth = useStore(s => s.rightPanelWidth) + const nodePanelWidth = useStore(s => s.nodePanelWidth) + const { userProfile } = useAppContext() const commentDetailCacheRef = useRef>(commentDetailCache) const activeCommentIdRef = useRef(null) @@ -113,16 +116,63 @@ export const useWorkflowComment = () => { console.log('Comment created successfully:', newComment) + const createdAt = (newComment as any)?.created_at + const createdByAccount = { + id: userProfile?.id ?? '', + name: userProfile?.name ?? '', + email: userProfile?.email ?? '', + avatar_url: userProfile?.avatar_url || userProfile?.avatar || undefined, + } + + const composedComment: WorkflowCommentList = { + id: newComment.id, + position_x: flowPosition.x, + position_y: flowPosition.y, + content, + created_by: createdByAccount.id, + created_by_account: createdByAccount, + created_at: createdAt, + updated_at: createdAt, + resolved: false, + mention_count: mentionedUserIds.length, + reply_count: 0, + participants: createdByAccount.id ? [createdByAccount] : [], + } + + const composedDetail: WorkflowCommentDetail = { + id: newComment.id, + position_x: flowPosition.x, + position_y: flowPosition.y, + content, + created_by: createdByAccount.id, + created_by_account: createdByAccount, + created_at: createdAt, + updated_at: createdAt, + resolved: false, + replies: [], + mentions: mentionedUserIds.map(mentionedId => ({ + mentioned_user_id: mentionedId, + mentioned_user_account: null, + reply_id: null, + })), + } + + setComments([...comments, composedComment]) + commentDetailCacheRef.current = { + ...commentDetailCacheRef.current, + [newComment.id]: composedDetail, + } + setCommentDetailCache(commentDetailCacheRef.current) + collaborationManager.emitCommentsUpdate(appId) - await loadComments() setPendingComment(null) } catch (error) { console.error('Failed to create comment:', error) setPendingComment(null) } - }, [appId, pendingComment, setPendingComment, loadComments, reactflow]) + }, [appId, pendingComment, setPendingComment, reactflow, comments, setComments, userProfile, setCommentDetailCache]) const handleCommentCancel = useCallback(() => { setPendingComment(null) @@ -142,9 +192,16 @@ export const useWorkflowComment = () => { const cachedDetail = commentDetailCacheRef.current[comment.id] setActiveComment(cachedDetail || comment) - let horizontalOffsetPx = 220 + const hasSelectedNode = reactflow.getNodes().some(node => node.data?.selected) + const commentPanelWidth = controlMode === ControlMode.Comment ? 420 : 0 + const fallbackPanelWidth = (hasSelectedNode ? nodePanelWidth : 0) + commentPanelWidth + const effectivePanelWidth = Math.max(rightPanelWidth ?? 0, fallbackPanelWidth) + + const baseHorizontalOffsetPx = 220 + const panelCompensationPx = effectivePanelWidth / 2 + const desiredHorizontalOffsetPx = baseHorizontalOffsetPx + panelCompensationPx const maxOffset = Math.max(0, (window.innerWidth / 2) - 60) - horizontalOffsetPx = Math.min(horizontalOffsetPx, maxOffset) + const horizontalOffsetPx = Math.min(desiredHorizontalOffsetPx, maxOffset) reactflow.setCenter( comment.position_x + horizontalOffsetPx, @@ -175,7 +232,18 @@ export const useWorkflowComment = () => { finally { setActiveCommentLoading(false) } - }, [appId, reactflow, setActiveComment, setActiveCommentId, setActiveCommentLoading, setCommentDetailCache, setControlMode, setPendingComment]) + }, [ + appId, + controlMode, + nodePanelWidth, + reactflow, + rightPanelWidth, + setActiveComment, + setActiveCommentId, + setActiveCommentLoading, + setCommentDetailCache, + setPendingComment, + ]) const handleCommentResolve = useCallback(async (commentId: string) => { if (!appId) return diff --git a/web/app/components/workflow/index.tsx b/web/app/components/workflow/index.tsx index ce150703a4..1108069c6a 100644 --- a/web/app/components/workflow/index.tsx +++ b/web/app/components/workflow/index.tsx @@ -422,7 +422,7 @@ export const Workflow: FC = memo(({
@@ -528,7 +528,7 @@ export const Workflow: FC = memo(({ defaultViewport={viewport} multiSelectionKeyCode={null} deleteKeyCode={null} - nodesDraggable={!nodesReadOnly} + nodesDraggable={!nodesReadOnly && controlMode !== ControlMode.Comment} nodesConnectable={!nodesReadOnly} nodesFocusable={!nodesReadOnly} edgesFocusable={!nodesReadOnly} diff --git a/web/app/components/workflow/nodes/_base/node.tsx b/web/app/components/workflow/nodes/_base/node.tsx index 5e774f7d31..e9a4d2dc57 100644 --- a/web/app/components/workflow/nodes/_base/node.tsx +++ b/web/app/components/workflow/nodes/_base/node.tsx @@ -19,6 +19,7 @@ import { useTranslation } from 'react-i18next' import type { NodeProps } from '../../types' import { BlockEnum, + ControlMode, NodeRunningStatus, } from '../../types' import { @@ -72,6 +73,7 @@ const BaseNode: FC = ({ const { userProfile } = useAppContext() const appId = useStore(s => s.appId) const { nodePanelPresence } = useCollaboration(appId as string) + const controlMode = useStore(s => s.controlMode) const currentUserPresence = useMemo(() => { const userId = userProfile?.id || '' @@ -196,6 +198,7 @@ const BaseNode: FC = ({ className={cn( 'group relative pb-1 shadow-xs', 'rounded-[15px] border border-transparent', + (controlMode === ControlMode.Comment) && 'hover:cursor-none', (data.type !== BlockEnum.Iteration && data.type !== BlockEnum.Loop) && 'w-[240px] bg-workflow-block-bg', (data.type === BlockEnum.Iteration || data.type === BlockEnum.Loop) && 'flex h-full w-full flex-col border-workflow-block-border bg-workflow-block-bg-transparent', !data._runningStatus && 'hover:shadow-lg', diff --git a/web/app/components/workflow/nodes/iteration/use-interactions.ts b/web/app/components/workflow/nodes/iteration/use-interactions.ts index 5d61d0267f..7cc7857deb 100644 --- a/web/app/components/workflow/nodes/iteration/use-interactions.ts +++ b/web/app/components/workflow/nodes/iteration/use-interactions.ts @@ -26,7 +26,9 @@ export const useNodeIterationInteractions = () => { const { nodes, setNodes } = collaborativeWorkflow.getState() const currentNode = nodes.find(n => n.id === nodeId)! - const childrenNodes = nodes.filter(n => n.parentId === nodeId) + const childrenNodes = nodes.filter(n => n.parentId === nodeId && n.type !== CUSTOM_ITERATION_START_NODE) + if (!childrenNodes.length) + return let rightNode: Node let bottomNode: Node diff --git a/web/app/components/workflow/nodes/loop/use-config.ts b/web/app/components/workflow/nodes/loop/use-config.ts index 2047b0d2d5..e882e6b62d 100644 --- a/web/app/components/workflow/nodes/loop/use-config.ts +++ b/web/app/components/workflow/nodes/loop/use-config.ts @@ -1,5 +1,6 @@ import { useCallback, + useEffect, useRef, } from 'react' import produce from 'immer' @@ -26,6 +27,9 @@ const useConfig = (id: string, payload: LoopNodeType) => { const { inputs, setInputs } = useNodeCrud(id, payload) const inputsRef = useRef(inputs) + useEffect(() => { + inputsRef.current = inputs + }, [inputs]) const handleInputsChange = useCallback((newInputs: LoopNodeType) => { inputsRef.current = newInputs setInputs(newInputs) diff --git a/web/app/components/workflow/nodes/loop/use-interactions.ts b/web/app/components/workflow/nodes/loop/use-interactions.ts index 8e8622a554..63468edaac 100644 --- a/web/app/components/workflow/nodes/loop/use-interactions.ts +++ b/web/app/components/workflow/nodes/loop/use-interactions.ts @@ -22,7 +22,9 @@ export const useNodeLoopInteractions = () => { const handleNodeLoopRerender = useCallback((nodeId: string) => { const { nodes, setNodes } = collaborativeWorkflow.getState() const currentNode = nodes.find(n => n.id === nodeId)! - const childrenNodes = nodes.filter(n => n.parentId === nodeId) + const childrenNodes = nodes.filter(n => n.parentId === nodeId && n.type !== CUSTOM_LOOP_START_NODE) + if (!childrenNodes.length) + return let rightNode: Node let bottomNode: Node diff --git a/web/app/components/workflow/operator/zoom-in-out.tsx b/web/app/components/workflow/operator/zoom-in-out.tsx index 24035917a7..4db3841bf2 100644 --- a/web/app/components/workflow/operator/zoom-in-out.tsx +++ b/web/app/components/workflow/operator/zoom-in-out.tsx @@ -229,7 +229,7 @@ const ZoomInOut: FC = ({
- +
{ ZOOM_IN_OUT_OPTIONS.map((options, i) => ( diff --git a/web/i18n/de-DE/workflow.ts b/web/i18n/de-DE/workflow.ts index 06d3821e6a..a01e6b78ca 100644 --- a/web/i18n/de-DE/workflow.ts +++ b/web/i18n/de-DE/workflow.ts @@ -328,6 +328,9 @@ const translation = { zoomTo50: 'Auf 50% vergrößern', zoomTo100: 'Auf 100% vergrößern', zoomToFit: 'An Bildschirm anpassen', + showUserComments: 'Kommentare', + showUserCursors: 'Cursor von Mitarbeitenden', + showMiniMap: 'Minikarte', selectionAlignment: 'Ausrichtung der Auswahl', alignLeft: 'Links', alignTop: 'Nach oben', diff --git a/web/i18n/es-ES/workflow.ts b/web/i18n/es-ES/workflow.ts index d78850ad83..eb62110c21 100644 --- a/web/i18n/es-ES/workflow.ts +++ b/web/i18n/es-ES/workflow.ts @@ -328,6 +328,9 @@ const translation = { zoomTo50: 'Zoom al 50%', zoomTo100: 'Zoom al 100%', zoomToFit: 'Ajustar al tamaño', + showUserComments: 'Comentarios', + showUserCursors: 'Cursores de colaboradores', + showMiniMap: 'Minimapa', alignTop: 'Arriba', alignBottom: 'Abajo', alignNodes: 'Alinear nodos', diff --git a/web/i18n/fa-IR/workflow.ts b/web/i18n/fa-IR/workflow.ts index addb9618b9..5191dbfe70 100644 --- a/web/i18n/fa-IR/workflow.ts +++ b/web/i18n/fa-IR/workflow.ts @@ -328,6 +328,9 @@ const translation = { zoomTo50: 'بزرگ‌نمایی به 50%', zoomTo100: 'بزرگ‌نمایی به 100%', zoomToFit: 'تناسب با اندازه', + showUserComments: 'نظرات', + showUserCursors: 'نشانگرهای همکاران', + showMiniMap: 'نقشه کوچک', horizontal: 'افقی', alignBottom: 'پایین', alignRight: 'راست', diff --git a/web/i18n/fr-FR/workflow.ts b/web/i18n/fr-FR/workflow.ts index 104606789f..b4e7197f7a 100644 --- a/web/i18n/fr-FR/workflow.ts +++ b/web/i18n/fr-FR/workflow.ts @@ -328,6 +328,9 @@ const translation = { zoomTo50: 'Zoomer à 50%', zoomTo100: 'Zoomer à 100%', zoomToFit: 'Zoomer pour ajuster', + showUserComments: 'Commentaires', + showUserCursors: 'Curseurs des collaborateurs', + showMiniMap: 'Mini-carte', alignBottom: 'Bas', alignLeft: 'Gauche', alignCenter: 'Centre', diff --git a/web/i18n/hi-IN/workflow.ts b/web/i18n/hi-IN/workflow.ts index b6330fe79f..d96c8bea38 100644 --- a/web/i18n/hi-IN/workflow.ts +++ b/web/i18n/hi-IN/workflow.ts @@ -339,6 +339,9 @@ const translation = { zoomTo50: '50% पर ज़ूम करें', zoomTo100: '100% पर ज़ूम करें', zoomToFit: 'फिट करने के लिए ज़ूम करें', + showUserComments: 'टिप्पणियाँ', + showUserCursors: 'सहयोगी कर्सर', + showMiniMap: 'मिनी मानचित्र', alignRight: 'दाएं', alignLeft: 'बाएं', alignTop: 'शीर्ष', diff --git a/web/i18n/id-ID/workflow.ts b/web/i18n/id-ID/workflow.ts index 63496f58b5..372a6a70df 100644 --- a/web/i18n/id-ID/workflow.ts +++ b/web/i18n/id-ID/workflow.ts @@ -317,6 +317,9 @@ const translation = { alignCenter: 'Pusat', zoomOut: 'Perkecil', zoomToFit: 'Perbesar agar sesuai', + showUserComments: 'Komentar', + showUserCursors: 'Kursor kolaborator', + showMiniMap: 'Peta mini', vertical: 'Vertikal', alignTop: 'Puncak', alignMiddle: 'Tengah', diff --git a/web/i18n/it-IT/workflow.ts b/web/i18n/it-IT/workflow.ts index a94b7d06ca..cb3bfecd9b 100644 --- a/web/i18n/it-IT/workflow.ts +++ b/web/i18n/it-IT/workflow.ts @@ -342,6 +342,9 @@ const translation = { zoomTo50: 'Zoom al 50%', zoomTo100: 'Zoom al 100%', zoomToFit: 'Zoom per Adattare', + showUserComments: 'Commenti', + showUserCursors: 'Cursori dei collaboratori', + showMiniMap: 'Mini mappa', alignRight: 'A destra', selectionAlignment: 'Allineamento della selezione', alignBottom: 'In basso', diff --git a/web/i18n/ja-JP/workflow.ts b/web/i18n/ja-JP/workflow.ts index f3fca315d0..09d434f799 100644 --- a/web/i18n/ja-JP/workflow.ts +++ b/web/i18n/ja-JP/workflow.ts @@ -333,6 +333,9 @@ const translation = { zoomTo50: '50% サイズ', zoomTo100: '等倍表示', zoomToFit: '画面に合わせる', + showUserComments: 'コメント', + showUserCursors: '協働者のカーソル', + showMiniMap: 'ミニマップ', horizontal: '水平', alignBottom: '下', alignNodes: 'ノードを整列', diff --git a/web/i18n/ko-KR/workflow.ts b/web/i18n/ko-KR/workflow.ts index 73eb5da3dd..6cda796f39 100644 --- a/web/i18n/ko-KR/workflow.ts +++ b/web/i18n/ko-KR/workflow.ts @@ -349,6 +349,9 @@ const translation = { zoomTo50: '50% 로 확대', zoomTo100: '100% 로 확대', zoomToFit: '화면에 맞게 확대', + showUserComments: '댓글', + showUserCursors: '협업자 커서', + showMiniMap: '미니맵', alignCenter: '중앙', alignRight: '오른쪽', alignLeft: '왼쪽', diff --git a/web/i18n/pl-PL/workflow.ts b/web/i18n/pl-PL/workflow.ts index 1d2a892941..8667abd217 100644 --- a/web/i18n/pl-PL/workflow.ts +++ b/web/i18n/pl-PL/workflow.ts @@ -302,6 +302,9 @@ const translation = { alignCenter: 'Centrum', alignRight: 'Prawy', alignNodes: 'Wyrównywanie węzłów', + showUserComments: 'Komentarze', + showUserCursors: 'Kursory współpracowników', + showMiniMap: 'Minimapa', selectionAlignment: 'Wyrównanie zaznaczenia', horizontal: 'Poziomy', distributeVertical: 'Rozmieść pionowo', diff --git a/web/i18n/pt-BR/workflow.ts b/web/i18n/pt-BR/workflow.ts index 5610cacc13..ce5929bc35 100644 --- a/web/i18n/pt-BR/workflow.ts +++ b/web/i18n/pt-BR/workflow.ts @@ -302,6 +302,9 @@ const translation = { alignLeft: 'Esquerda', alignBottom: 'Inferior', distributeHorizontal: 'Distribuir horizontalmente', + showUserComments: 'Comentários', + showUserCursors: 'Cursores dos colaboradores', + showMiniMap: 'Minimapa', alignMiddle: 'Meio', alignRight: 'Direita', horizontal: 'Horizontal', diff --git a/web/i18n/ro-RO/workflow.ts b/web/i18n/ro-RO/workflow.ts index d2239e7979..96ee9a79ce 100644 --- a/web/i18n/ro-RO/workflow.ts +++ b/web/i18n/ro-RO/workflow.ts @@ -304,6 +304,9 @@ const translation = { alignMiddle: 'Mijloc', distributeVertical: 'Distribuie vertical', alignCenter: 'Centru', + showUserComments: 'Comentarii', + showUserCursors: 'Cursoarele colaboratorilor', + showMiniMap: 'Mini-hartă', distributeHorizontal: 'Distribuie orizontal', alignBottom: 'Jos', alignTop: 'Sus', diff --git a/web/i18n/ru-RU/workflow.ts b/web/i18n/ru-RU/workflow.ts index 2345f3447b..1a59672d42 100644 --- a/web/i18n/ru-RU/workflow.ts +++ b/web/i18n/ru-RU/workflow.ts @@ -300,6 +300,9 @@ const translation = { alignBottom: 'Вниз', alignRight: 'Вправо', distributeHorizontal: 'Распределить по горизонтали', + showUserComments: 'Комментарии', + showUserCursors: 'Курсоры участников', + showMiniMap: 'Мини-карта', alignMiddle: 'По центру', vertical: 'Вертикальный', alignCenter: 'Центр', diff --git a/web/i18n/sl-SI/workflow.ts b/web/i18n/sl-SI/workflow.ts index 7a167c236f..774c5aa617 100644 --- a/web/i18n/sl-SI/workflow.ts +++ b/web/i18n/sl-SI/workflow.ts @@ -300,6 +300,9 @@ const translation = { alignBottom: 'Spodaj', alignCenter: 'Center', distributeVertical: 'Razporedi navpično', + showUserComments: 'Komentarji', + showUserCursors: 'Kazalci sodelavcev', + showMiniMap: 'Mini zemljevid', alignRight: 'Desno', alignTop: 'Vrh', vertical: 'Navpičen', diff --git a/web/i18n/th-TH/workflow.ts b/web/i18n/th-TH/workflow.ts index 1cea01690a..264d097514 100644 --- a/web/i18n/th-TH/workflow.ts +++ b/web/i18n/th-TH/workflow.ts @@ -302,6 +302,9 @@ const translation = { horizontal: 'แนวนอน', vertical: 'แนวตั้ง', alignTop: 'ด้านบน', + showUserComments: 'ความคิดเห็น', + showUserCursors: 'เคอร์เซอร์ของผู้ร่วมงาน', + showMiniMap: 'แผนที่ย่อ', distributeVertical: 'ระยะห่างแนวตั้ง', alignLeft: 'ซ้าย', selectionAlignment: 'การจัดตําแหน่งการเลือก', diff --git a/web/i18n/tr-TR/workflow.ts b/web/i18n/tr-TR/workflow.ts index dfab5c2c0c..f0d027f5ea 100644 --- a/web/i18n/tr-TR/workflow.ts +++ b/web/i18n/tr-TR/workflow.ts @@ -301,6 +301,9 @@ const translation = { alignLeft: 'Sol', alignNodes: 'Düğümleri Hizala', vertical: 'Dikey', + showUserComments: 'Yorumlar', + showUserCursors: 'İşbirlikçi imleçleri', + showMiniMap: 'Mini harita', alignRight: 'Sağ', alignTop: 'Üst', alignBottom: 'Alt', diff --git a/web/i18n/uk-UA/workflow.ts b/web/i18n/uk-UA/workflow.ts index 09f2b71eea..8de3dd639d 100644 --- a/web/i18n/uk-UA/workflow.ts +++ b/web/i18n/uk-UA/workflow.ts @@ -302,6 +302,9 @@ const translation = { alignBottom: 'Низ', alignLeft: 'Ліворуч', alignTop: 'Верх', + showUserComments: 'Коментарі', + showUserCursors: 'Курсори співучасників', + showMiniMap: 'Мінікарта', horizontal: 'Горизонтальний', alignMiddle: 'По центру', distributeVertical: 'Розподілити по вертикалі', diff --git a/web/i18n/vi-VN/workflow.ts b/web/i18n/vi-VN/workflow.ts index 27d19a37f4..aae85bb893 100644 --- a/web/i18n/vi-VN/workflow.ts +++ b/web/i18n/vi-VN/workflow.ts @@ -300,6 +300,9 @@ const translation = { alignMiddle: 'Giữa', alignRight: 'Phải', alignNodes: 'Căn chỉnh các nút', + showUserComments: 'Bình luận', + showUserCursors: 'Con trỏ của cộng tác viên', + showMiniMap: 'Bản đồ nhỏ', alignLeft: 'Trái', horizontal: 'Ngang', alignCenter: 'Giữa', diff --git a/web/i18n/zh-Hans/workflow.ts b/web/i18n/zh-Hans/workflow.ts index ae7cc427e5..81ba647bf5 100644 --- a/web/i18n/zh-Hans/workflow.ts +++ b/web/i18n/zh-Hans/workflow.ts @@ -334,6 +334,9 @@ const translation = { zoomTo50: '缩放到 50%', zoomTo100: '放大到 100%', zoomToFit: '自适应视图', + showUserComments: '评论', + showUserCursors: '协作者光标', + showMiniMap: '小地图', alignNodes: '对齐节点', alignLeft: '左对齐', alignCenter: '居中对齐', diff --git a/web/i18n/zh-Hant/workflow.ts b/web/i18n/zh-Hant/workflow.ts index bb580c3de4..94d46bd2dc 100644 --- a/web/i18n/zh-Hant/workflow.ts +++ b/web/i18n/zh-Hant/workflow.ts @@ -328,6 +328,9 @@ const translation = { zoomTo50: '縮放到 50%', zoomTo100: '放大到 100%', zoomToFit: '自適應視圖', + showUserComments: '評論', + showUserCursors: '協作者游標', + showMiniMap: '小地圖', alignNodes: '對齊節點', distributeVertical: '垂直等間距', alignLeft: '左對齊',