Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { useEffect, useState } from 'react'

import { useVersionControl } from '../../../../../middleware/shared/providers'
import { useOpenPLCStore } from '../../../../store'
import { notifyNoWritePermission } from '../../../../utils/notify-no-write-permission'

type CreateBranchModalProps = {
isOpen: boolean
Expand All @@ -12,8 +13,7 @@

export function CreateBranchModal({ isOpen, projectId, onClose, onCreated }: CreateBranchModalProps) {
const versionControl = useVersionControl()
const isReadOnly = useOpenPLCStore((s) => s.workspace.isReadOnly)
const openReadOnlyModal = useOpenPLCStore((s) => s.modalActions.openModal)
const canEdit = useOpenPLCStore((s) => s.workspace.canEdit)
const [name, setName] = useState('')
const [error, setError] = useState('')
const [isPending, setIsPending] = useState(false)
Expand All @@ -34,20 +34,17 @@
}
}, [isOpen])

// Read-only ⇒ close this modal and surface the read-only/fork modal
// instead. A useEffect (not an early-return rewrite) avoids breaking
// hook ordering for the rest of the component when isOpen flips.
useEffect(() => {
if (isOpen && isReadOnly) {
onClose()
openReadOnlyModal('read-only-project')
}
}, [isOpen, isReadOnly, onClose, openReadOnlyModal])

if (!isOpen) return null

const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()

// No write permission ⇒ skip the doomed backend write and warn.
if (!canEdit) {
notifyNoWritePermission('create branches in')
return
}

const trimmed = name.trim()

if (!trimmed) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import * as Popover from '@radix-ui/react-popover'
import { useMemo, useState } from 'react'

import { useVersionControl } from '../../../../../middleware/shared/providers'
import { useOpenPLCStore } from '../../../../store'
import { cn } from '../../../../utils/cn'
import { notifyNoWritePermission } from '../../../../utils/notify-no-write-permission'
import { getBranchNameFeedback } from '../../../../utils/sanitize-branch-name'

type CreateBranchPopoverProps = {
Expand All @@ -14,8 +15,7 @@

export function CreateBranchPopover({ projectId, onCreated, onCloseParent }: CreateBranchPopoverProps) {
const versionControl = useVersionControl()
const isReadOnly = useOpenPLCStore((s) => s.workspace.isReadOnly)
const openModal = useOpenPLCStore((s) => s.modalActions.openModal)
const canEdit = useOpenPLCStore((s) => s.workspace.canEdit)
const [isOpen, setIsOpen] = useState(false)
const [name, setName] = useState('')
const [error, setError] = useState('')
Expand Down Expand Up @@ -64,10 +64,10 @@
<Popover.Root
open={isOpen}
onOpenChange={(open) => {
// Read-only ⇒ redirect to the fork-or-cancel modal instead of
// opening a branch form the backend would 403 anyway.
if (open && isReadOnly) {
openModal('read-only-project')
// No write permission ⇒ warn instead of opening a branch form the
// backend would reject anyway.
if (open && !canEdit) {
notifyNoWritePermission('create branches in')
return
}
setIsOpen(open)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { useEffect, useState } from 'react'

import type { Branch } from '../../../../../middleware/shared/ports/version-control-port'
import { useVersionControl } from '../../../../../middleware/shared/providers'
import { useOpenPLCStore } from '../../../../store'
import { notifyNoWritePermission } from '../../../../utils/notify-no-write-permission'
import { toast } from '../../../../utils/toast'

type DeleteBranchModalProps = {
Expand All @@ -15,8 +16,7 @@

export function DeleteBranchModal({ isOpen, projectId, branch, onClose, onDeleted }: DeleteBranchModalProps) {
const versionControl = useVersionControl()
const isReadOnly = useOpenPLCStore((s) => s.workspace.isReadOnly)
const openReadOnlyModal = useOpenPLCStore((s) => s.modalActions.openModal)
const canEdit = useOpenPLCStore((s) => s.workspace.canEdit)
const [isPending, setIsPending] = useState(false)

useEffect(() => {
Expand All @@ -28,19 +28,15 @@
return () => document.removeEventListener('keydown', handleKeyDown)
}, [isOpen, isPending, onClose])

// Same pattern as CreateBranchModal — if a read-only project is open,
// close this dialog and route the user to the fork affordance.
useEffect(() => {
if (isOpen && isReadOnly) {
onClose()
openReadOnlyModal('read-only-project')
}
}, [isOpen, isReadOnly, onClose, openReadOnlyModal])

if (!isOpen || !branch) return null

const handleDelete = () => {
if (!versionControl) return
// No write permission ⇒ skip the doomed backend write and warn.
if (!canEdit) {
notifyNoWritePermission('delete branches in')
return
}

setIsPending(true)
versionControl
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
/* eslint-disable @typescript-eslint/no-misused-promises */
import * as Popover from '@radix-ui/react-popover'

Expand Down Expand Up @@ -128,23 +128,10 @@
serverActions: { create: createServer },
remoteDeviceActions: { create: createRemoteDevice },
deviceAvailableOptions: { availableBoards },
modalActions: { openModal },
} = useOpenPLCStore()
const isReadOnly = useOpenPLCStore((state) => state.workspace.isReadOnly)
const deviceBoard = useOpenPLCStore((state) => state.deviceDefinitions.configuration.deviceBoard)
const [isOpen, setIsOpen] = useState(false)

// Read-only ⇒ the create-element popover/menu just routes to the
// fork-or-cancel modal so the user knows why the affordance exists
// but can't make changes that wouldn't persist.
const handleOpen = (next: boolean) => {
if (next && isReadOnly) {
openModal('read-only-project')
return
}
setIsOpen(next)
}

const currentBoardInfo = availableBoards.get(deviceBoard)
const isArduinoTarget = checkIsArduinoTarget(currentBoardInfo)
const isSimulator = isSimulatorTarget(currentBoardInfo)
Expand Down Expand Up @@ -231,15 +218,11 @@
}

const handleMouseEnter = () => {
if (isReadOnly) {
openModal('read-only-project')
return
}
setIsOpen(true)
}

return (
<Popover.Root open={isOpen} onOpenChange={handleOpen}>
<Popover.Root open={isOpen} onOpenChange={setIsOpen}>
<Popover.Trigger
onMouseEnter={handleMouseEnter}
id={`create-${target}-trigger`}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { ComponentPropsWithoutRef } from 'react'

import { GraphicalEditorActiveProvider } from './active-context'
Expand Down Expand Up @@ -32,7 +32,9 @@
return (
<GraphicalEditorActiveProvider pouName={name} isActive={isActive}>
<div className='relative h-full w-full overflow-y-auto'>
{readOnly && <div className='absolute inset-0 z-10 cursor-not-allowed' title='Read-only' />}
{readOnly && (
<div className='absolute inset-0 z-10 cursor-not-allowed' title='Read-only: viewing historical commit' />
)}
<div className={`h-full w-full${readOnly ? ' pointer-events-none' : ''}`}>
<EditorComponent />
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import './configs'

import { Editor as PrimitiveEditor } from '@monaco-editor/react'
Expand Down Expand Up @@ -143,7 +143,6 @@
workspace: {
systemConfigs: { shouldUseDarkMode },
isDebuggerVisible,
isReadOnly,
fbSelectedInstance,
fbDebugInstances,
},
Expand Down Expand Up @@ -429,13 +428,10 @@
return () => disposable.dispose()
}, [editorMounted])

// Update readOnly when debugger visibility or project read-only flag changes.
// Debugger visibility forces read-only for safety; the project's own
// read-only flag (no edit permission) does the same so users browsing
// someone else's project can't make local modifications they couldn't save.
// Update readOnly when debugger visibility changes (editor-only)
useEffect(() => {
editorRef.current?.updateOptions({ readOnly: isDebuggerVisible || isReadOnly })
}, [isDebuggerVisible, isReadOnly])
editorRef.current?.updateOptions({ readOnly: isDebuggerVisible })
}, [isDebuggerVisible])

// Apply programmatic cursor jumps (e.g. clicking a compile error in
// the console) to an already-mounted editor. The onMount path
Expand Down Expand Up @@ -1240,7 +1236,7 @@
const monacoEditorUserOptions: monacoEditorOptionsType = {
minimap: { enabled: false },
dropIntoEditor: { enabled: true },
readOnly: isDebuggerVisible || isReadOnly,
readOnly: isDebuggerVisible,
// Lock indentation to 4 spaces across every language Monaco
// hosts (ST / IL / Python / C++). Without this Monaco's
// `detectIndentation` heuristic kicks in on the existing model
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import Editor from '@monaco-editor/react'
import { File, Folder, FolderOpen, X } from 'lucide-react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
Expand All @@ -10,6 +10,7 @@
import { CreateEditorObjectFromTab } from '../../../../store/slices/tabs/utils'
import type { PendingChangeStatus } from '../../../../store/slices/version-control/types'
import { cn } from '../../../../utils/cn'
import { notifyNoWritePermission } from '../../../../utils/notify-no-write-permission'
import { isSystemFile } from '../../../../utils/system-files'
import { toast } from '../../../../utils/toast'
import { DiscardConfirmationModal } from './modals/discard-confirmation-modal'
Expand Down Expand Up @@ -283,8 +284,7 @@
tabsActions: { updateTabs },
editorActions: { setEditor, addModel, getEditorFromEditors },
} = useOpenPLCStore()
const isReadOnly = useOpenPLCStore((s) => s.workspace.isReadOnly)
const openReadOnlyModal = useOpenPLCStore((s) => s.modalActions.openModal)
const canEdit = useOpenPLCStore((s) => s.workspace.canEdit)

const pous = project.data.pous

Expand Down Expand Up @@ -472,10 +472,9 @@

const handleCommit = async () => {
if (!canCommit || !versionControl) return
// No edit permission ⇒ open the fork-or-cancel affordance instead
// of letting the backend 403 the commit silently.
if (isReadOnly) {
openReadOnlyModal('read-only-project')
// No write permission ⇒ skip the doomed backend commit and warn.
if (!canEdit) {
notifyNoWritePermission('commit to')
return
}

Expand Down Expand Up @@ -541,6 +540,11 @@

const handleDiscard = async () => {
if (!versionControl) return
// No write permission ⇒ skip the doomed backend discard and warn.
if (!canEdit) {
notifyNoWritePermission('discard changes in')
return
}

setIsDiscarding(true)
setErrorMessage(null)
Expand Down Expand Up @@ -573,6 +577,11 @@

const handleStash = async (stashMessage: string) => {
if (!versionControl) return
// No write permission ⇒ skip the doomed backend stash and warn.
if (!canEdit) {
notifyNoWritePermission('stash changes in')
return
}

setIsStashing(true)
setErrorMessage(null)
Expand Down Expand Up @@ -723,25 +732,23 @@
</div>
<div className='flex gap-2'>
<button
onClick={() => (isReadOnly ? openReadOnlyModal('read-only-project') : void handleCommit())}
disabled={(!canCommit && !isReadOnly) || isCommitting}
title={isReadOnly ? 'Read-only project — fork to commit' : undefined}
onClick={() => void handleCommit()}
disabled={!canCommit || isCommitting}
className='flex-1 rounded-md bg-blue-500 px-3 py-1.5 text-xs font-medium text-white transition-colors duration-150 hover:bg-blue-600 disabled:cursor-not-allowed disabled:opacity-50'
>
{isCommitting ? 'Committing...' : 'Commit'}
</button>
<button
onClick={() => (isReadOnly ? openReadOnlyModal('read-only-project') : setShowStashModal(true))}
disabled={(selectedFiles.size === 0 && !isReadOnly) || isStashing}
title={isReadOnly ? 'Read-only project — fork to stash' : 'Stash selected changes for later'}
onClick={() => setShowStashModal(true)}
disabled={selectedFiles.size === 0 || isStashing}
title='Stash selected changes for later'
className='rounded-md bg-neutral-100 px-3 py-1.5 text-xs font-medium text-neutral-700 transition-colors duration-150 hover:bg-blue-50 hover:text-blue-600 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-neutral-800 dark:text-neutral-300 dark:hover:bg-blue-900/30 dark:hover:text-blue-400'
>
{isStashing ? 'Stashing...' : 'Stash'}
</button>
<button
onClick={() => (isReadOnly ? openReadOnlyModal('read-only-project') : setShowDiscardModal(true))}
disabled={(selectedFiles.size === 0 && !isReadOnly) || isDiscarding}
title={isReadOnly ? 'Read-only project — fork to discard' : undefined}
onClick={() => setShowDiscardModal(true)}
disabled={selectedFiles.size === 0 || isDiscarding}
className='rounded-md bg-neutral-100 px-3 py-1.5 text-xs font-medium text-neutral-700 transition-colors duration-150 hover:bg-red-50 hover:text-red-600 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-neutral-800 dark:text-neutral-300 dark:hover:bg-red-900/30 dark:hover:text-red-400'
>
Discard
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { ChevronDown, ChevronRight, File } from 'lucide-react'
import { useState } from 'react'

Expand All @@ -5,6 +5,7 @@
import { useNavigation, useProject, useVersionControl } from '../../../../../middleware/shared/providers'
import { useOpenPLCStore } from '../../../../store'
import { cn } from '../../../../utils/cn'
import { notifyNoWritePermission } from '../../../../utils/notify-no-write-permission'
import { toast } from '../../../../utils/toast'
import { RestoreConfirmationModal } from './modals/restore-confirmation-modal'

Expand All @@ -22,6 +23,7 @@
},
sharedWorkspaceActions,
} = useOpenPLCStore()
const canEdit = useOpenPLCStore((s) => s.workspace.canEdit)
const projectPort = useProject()

const [showRestoreModal, setShowRestoreModal] = useState(false)
Expand Down Expand Up @@ -71,6 +73,12 @@

const handleRestore = async () => {
if (!versionControl) return
// Restore overwrites the working tree from a past commit — a backend
// write. No write permission ⇒ skip it and warn.
if (!canEdit) {
notifyNoWritePermission('restore commits in')
return
}

setIsRestoring(true)
try {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { Archive, ChevronDown, Trash2 } from 'lucide-react'
import { useCallback, useEffect, useState } from 'react'

Expand All @@ -5,6 +5,7 @@
import { StashConflictError } from '../../../../../middleware/shared/ports/version-control-port'
import { useProject, useVersionControl } from '../../../../../middleware/shared/providers'
import { useOpenPLCStore } from '../../../../store'
import { notifyNoWritePermission } from '../../../../utils/notify-no-write-permission'
import { isSystemFile } from '../../../../utils/system-files'
import { toast } from '../../../../utils/toast'
import { StashDropConfirmationModal } from './modals/stash-drop-confirmation-modal'
Expand All @@ -31,8 +32,7 @@
const versionControl = useVersionControl()
const projectPort = useProject()
const { versionControlActions, sharedWorkspaceActions } = useOpenPLCStore()
const isReadOnly = useOpenPLCStore((s) => s.workspace.isReadOnly)
const openReadOnlyModal = useOpenPLCStore((s) => s.modalActions.openModal)
const canEdit = useOpenPLCStore((s) => s.workspace.canEdit)

const [stashes, setStashes] = useState<Stash[]>([])
const [isLoading, setIsLoading] = useState(true)
Expand Down Expand Up @@ -81,8 +81,9 @@

const handleApplyOrPop = async (stash: Stash, pop: boolean) => {
if (!versionControl || busyRef) return
if (isReadOnly) {
openReadOnlyModal('read-only-project')
// No write permission ⇒ skip the doomed backend write and warn.
if (!canEdit) {
notifyNoWritePermission('modify stashes in')
return
}
setBusyRef(stash.hash)
Expand All @@ -109,6 +110,11 @@

const handleDrop = async () => {
if (!versionControl || !dropTarget) return
// No write permission ⇒ skip the doomed backend write and warn.
if (!canEdit) {
notifyNoWritePermission('modify stashes in')
return
}
setIsDropping(true)
try {
await versionControl.dropStash(projectId, dropTarget.hash)
Expand Down
Loading
Loading