refac
Some checks are pending
Python CI / Ruff Format (3.11) (push) Waiting to run
Python CI / Ruff Format (3.12) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/arm64 runner:ubuntu-24.04-arm], map[build_args: free_disk:false name:main suffix:]) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/arm64 runner:ubuntu-24.04-arm], map[build_args:USE_CUDA=true USE_CUDA_VER=cu126 free_disk:true name:cuda126 suffix:-cuda126]) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/arm64 runner:ubuntu-24.04-arm], map[build_args:USE_OLLAMA=true free_disk:false name:ollama suffix:-ollama]) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/arm64 runner:ubuntu-24.04-arm], map[build_args:USE_SLIM=true free_disk:false name:slim suffix:-slim]) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/amd64 runner:ubuntu-latest], map[build_args: free_disk:false name:main suffix:]) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/amd64 runner:ubuntu-latest], map[build_args:USE_CUDA=true USE_CUDA_VER=cu126 free_disk:true name:cuda126 suffix:-cuda126]) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/amd64 runner:ubuntu-latest], map[build_args:USE_CUDA=true free_disk:true name:cuda suffix:-cuda]) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/amd64 runner:ubuntu-latest], map[build_args:USE_OLLAMA=true free_disk:false name:ollama suffix:-ollama]) (push) Waiting to run
Create and publish Docker images with specific build args / build (map[arch:linux/amd64 runner:ubuntu-latest], map[build_args:USE_SLIM=true free_disk:false name:slim suffix:-slim]) (push) Waiting to run
Create and publish Docker images with specific build args / merge (map[name:cuda suffix:-cuda]) (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge (map[name:cuda126 suffix:-cuda126]) (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge (map[name:main suffix:]) (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge (map[name:ollama suffix:-ollama]) (push) Blocked by required conditions
Create and publish Docker images with specific build args / build (map[arch:linux/arm64 runner:ubuntu-24.04-arm], map[build_args:USE_CUDA=true free_disk:true name:cuda suffix:-cuda]) (push) Waiting to run
Create and publish Docker images with specific build args / merge (map[name:slim suffix:-slim]) (push) Blocked by required conditions
Create and publish Docker images with specific build args / notify-helm-charts (push) Blocked by required conditions
Create and publish Docker images with specific build args / copy-to-dockerhub (, main) (push) Blocked by required conditions
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda, cuda) (push) Blocked by required conditions
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda126, cuda126) (push) Blocked by required conditions
Create and publish Docker images with specific build args / copy-to-dockerhub (-ollama, ollama) (push) Blocked by required conditions
Create and publish Docker images with specific build args / copy-to-dockerhub (-slim, slim) (push) Blocked by required conditions
Frontend Build / Format & Build (push) Waiting to run
Frontend Build / Unit Tests (push) Waiting to run

This commit is contained in:
Timothy Jaeryang Baek 2026-08-09 13:22:46 -06:00
parent 5caa91a493
commit 2dadc5435a
7 changed files with 101 additions and 38 deletions

View file

@ -23,6 +23,7 @@ from open_webui.utils.json_codec import JSONCodec
from open_webui.utils.terminals import (
TERMINAL_CONTEXT_HEADER,
get_terminal_server_url,
is_terminal_orchestrator,
terminal_context_available,
terminal_context_config,
terminal_context_id,
@ -233,7 +234,7 @@ async def _resolve_authenticated_connection(ws: WebSocket, server_id: str):
The client must send ``{"type": "auth", "token": "<jwt>"}`` as its first
message after connecting.
Returns ``(user, connection, chat_id)`` on success, or ``None`` after
Returns ``(user, connection, chat_id, token)`` on success, or ``None`` after
closing *ws* with an appropriate error code.
"""
import asyncio
@ -247,7 +248,8 @@ async def _resolve_authenticated_connection(ws: WebSocket, server_id: str):
if payload.get('type') != 'auth':
await ws.close(code=4001, reason='Expected auth message')
return None
user = await get_verified_user_by_token(payload.get('token', ''), getattr(ws.app.state, 'redis', None))
token = payload.get('token', '')
user = await get_verified_user_by_token(token, getattr(ws.app.state, 'redis', None))
if user is None:
await ws.close(code=4001, reason='Invalid token')
return None
@ -279,7 +281,7 @@ async def _resolve_authenticated_connection(ws: WebSocket, server_id: str):
if not terminal_context_available(connection, 'chat'):
await ws.close(code=4003, reason='Terminal server is not available in chats')
return None
return user, connection, chat_id if isinstance(chat_id, str) else ''
return user, connection, chat_id if isinstance(chat_id, str) else '', token
@router.websocket('/{server_id}/api/terminals/{session_id}')
@ -292,14 +294,14 @@ async def ws_terminal(
Uses first-message auth: the client sends ``{"type": "auth", "token": "<jwt>"}``
as its first message. The proxy validates the JWT, then connects to the
upstream terminal server and authenticates with the server's API key.
upstream terminal server using the configured terminal auth mode.
"""
await ws.accept()
result = await _resolve_authenticated_connection(ws, server_id)
if result is None:
return
user, connection, chat_id = result
user, connection, chat_id, token = result
base_url = get_terminal_server_url(connection)
if not base_url:
@ -347,6 +349,8 @@ async def ws_terminal(
if auth_type == 'bearer':
key = normalize_bearer_token(connection.get('key', ''))
await upstream.send_str(_json.dumps({'type': 'auth', 'token': key}))
elif auth_type == 'session' and is_terminal_orchestrator(connection):
await upstream.send_str(_json.dumps({'type': 'auth', 'token': token}))
await publish_event(
app,

View file

@ -3,6 +3,12 @@ export type FileEntry = {
type: 'file' | 'directory';
size?: number;
modified?: number;
writable?: boolean;
};
export type TerminalFileList = {
entries: FileEntry[];
writable?: boolean;
};
export type ListeningPort = {
@ -85,7 +91,7 @@ export const listFiles = async (
apiKey: string,
path: string = '/',
sessionId?: string
): Promise<FileEntry[] | null> => {
): Promise<TerminalFileList | null> => {
// The endpoint uses `directory` as the query param name
const url = `${baseUrl.replace(/\/$/, '')}/files/list?directory=${encodeURIComponent(path)}`;
const headers: Record<string, string> = bearerHeaders(apiKey);
@ -99,7 +105,7 @@ export const listFiles = async (
console.error('open-terminal listFiles error:', err);
return null;
});
return res?.entries ?? null;
return res?.entries ? { entries: res.entries, writable: res.writable } : null;
};
export const readFile = async (

View file

@ -93,6 +93,7 @@
let currentPath = savedPath;
let fileRoot: TerminalFileRoot | null = null;
let entries: FileEntry[] = [];
let currentWritable = true;
let loading = false;
let error: string | null = null;
@ -175,6 +176,7 @@
// ── File preview state ───────────────────────────────────────────────
let selectedFile: string | null = null;
let selectedFileWritable = true;
let previewPort: number | null = null;
let fileContent: string | null = null;
let fileImageUrl: string | null = null;
@ -410,6 +412,7 @@
loading = true;
error = null;
selectedFile = null;
selectedFileWritable = true;
previewPort = null;
clearFilePreview();
clearSelection();
@ -428,7 +431,8 @@
'Failed to load directory. Check your Terminal connection in Settings → Integrations.';
entries = [];
} else {
entries = sortEntries(result);
currentWritable = result.writable !== false;
entries = sortEntries(result.entries);
}
};
@ -439,6 +443,7 @@
}
const filePath = `${currentPath}${entry.name}`;
selectedFileWritable = entry.writable !== false;
pushNavHistory(currentPath, filePath);
const terminal = selectedTerminal;
@ -566,6 +571,7 @@
// ── Drag-and-drop upload ─────────────────────────────────────────────
const handleDragOver = (e: DragEvent) => {
if (selectedFile) return;
if (!currentWritable) return;
if (!e.dataTransfer?.types.includes('Files')) return;
e.preventDefault();
e.stopPropagation();
@ -578,7 +584,7 @@
isDragOver = false;
const terminal = selectedTerminal;
if (selectedFile || !terminal) return;
if (selectedFile || !terminal || !currentWritable) return;
const droppedFiles = Array.from(e.dataTransfer?.files ?? []);
if (!droppedFiles.length) return;
@ -593,7 +599,7 @@
const handleUploadFiles = async (files: File[]) => {
const terminal = selectedTerminal;
if (!files.length || !terminal) return;
if (!files.length || !terminal || !currentWritable) return;
uploading = true;
for (const file of files) {
@ -605,6 +611,7 @@
// ── Folder creation ──────────────────────────────────────────────────
const startNewFolder = async () => {
if (!currentWritable) return;
creatingFolder = true;
newFolderName = '';
await tick();
@ -634,6 +641,7 @@
// ── File creation ────────────────────────────────────────────────────
const startNewFile = async () => {
if (!currentWritable) return;
creatingFile = true;
newFileName = '';
await tick();
@ -658,7 +666,7 @@
// ── Delete ───────────────────────────────────────────────────────────
const handleDelete = async (path: string, name: string) => {
const terminal = selectedTerminal;
if (!terminal) return;
if (!terminal || !currentWritable) return;
const result = await deleteEntry(terminal.url, terminal.key, path, chatId ?? undefined);
toast[result ? 'success' : 'error'](
@ -668,6 +676,7 @@
};
const requestDelete = (path: string, name: string) => {
if (!currentWritable) return;
deleteTarget = { path, name };
showDeleteConfirm = true;
};
@ -675,7 +684,7 @@
// ── Move (drag-and-drop) ────────────────────────────────────────────
const handleMove = async (source: string, destFolder: string) => {
const terminal = selectedTerminal;
if (!terminal) return;
if (!terminal || !currentWritable) return;
const fileName = source.split('/').pop() ?? '';
const destination = `${destFolder}${fileName}`;
@ -704,7 +713,7 @@
// ── Rename ──────────────────────────────────────────────────────────
const handleRename = async (oldPath: string, newName: string) => {
const terminal = selectedTerminal;
if (!terminal || !newName) return;
if (!terminal || !newName || !currentWritable) return;
const dir = oldPath.substring(0, oldPath.lastIndexOf('/') + 1) || currentPath;
const destination = `${dir}${newName}`;
@ -733,6 +742,13 @@
$: selectedCount = selectedEntries.size;
$: hasSelectedFiles = [...selectedEntries].some((p) => !p.endsWith('/'));
$: selectedEntriesWritable =
currentWritable &&
[...selectedEntries].every((path) => {
const name = path.replace(/\/$/, '').split('/').pop();
const entry = entries.find((item) => item.name === name);
return entry?.writable !== false;
});
const clearSelection = () => {
selectedEntries = new Set();
@ -792,7 +808,7 @@
const bulkDelete = async () => {
const terminal = selectedTerminal;
if (!terminal) return;
if (!terminal || !selectedEntriesWritable) return;
const paths = [...selectedEntries];
let ok = 0;
@ -1046,6 +1062,7 @@
breadcrumbs={buildBreadcrumbs(currentPath)}
{selectedFile}
{loading}
writable={currentWritable}
{canGoBack}
{canGoForward}
{sortBy}
@ -1143,9 +1160,9 @@
{#if isHtml && showRaw}
<Tooltip content={$i18n.t('Save')}>
<button
class="shrink-0 p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-400"
class="shrink-0 p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-400 disabled:opacity-30 disabled:hover:bg-transparent"
on:click={() => filePreviewRef?.saveCodeFile()}
disabled={saving}
disabled={saving || !selectedFileWritable}
aria-label={$i18n.t('Save')}
>
{#if saving}
@ -1171,9 +1188,9 @@
{:else if isMarkdown && showRaw}
<Tooltip content={$i18n.t('Save')}>
<button
class="shrink-0 p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-400"
class="shrink-0 p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-400 disabled:opacity-30 disabled:hover:bg-transparent"
on:click={() => filePreviewRef?.saveCodeFile()}
disabled={saving}
disabled={saving || !selectedFileWritable}
aria-label={$i18n.t('Save')}
>
{#if saving}
@ -1199,9 +1216,9 @@
{:else if isCode}
<Tooltip content={$i18n.t('Save')}>
<button
class="shrink-0 p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-400"
class="shrink-0 p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-400 disabled:opacity-30 disabled:hover:bg-transparent"
on:click={() => filePreviewRef?.saveCodeFile()}
disabled={saving}
disabled={saving || !selectedFileWritable}
aria-label={$i18n.t('Save')}
>
{#if saving}
@ -1243,9 +1260,9 @@
</Tooltip>
<Tooltip content={$i18n.t('Save')}>
<button
class="shrink-0 p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-400"
class="shrink-0 p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-400 disabled:opacity-30 disabled:hover:bg-transparent"
on:click={() => filePreviewRef?.saveEdit()}
disabled={saving}
disabled={saving || !selectedFileWritable}
aria-label={$i18n.t('Save')}
>
{#if saving}
@ -1269,8 +1286,9 @@
{:else}
<Tooltip content={$i18n.t('Edit')}>
<button
class="shrink-0 p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-400"
class="shrink-0 p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-400 disabled:opacity-30 disabled:hover:bg-transparent"
on:click={() => filePreviewRef?.startEdit()}
disabled={!selectedFileWritable}
aria-label={$i18n.t('Edit')}
>
<PenAlt className="size-3.5" />
@ -1309,7 +1327,7 @@
<Tooltip content={$i18n.t('Download')}>
<button
class="shrink-0 p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-400"
on:click={() => downloadFile(selectedFile)}
on:click={() => selectedFile && downloadFile(selectedFile)}
aria-label={$i18n.t('Download')}
>
<svg
@ -1335,6 +1353,7 @@
<BulkActionBar
count={selectedCount}
hasFiles={hasSelectedFiles}
canDelete={selectedEntriesWritable}
onDelete={() => {
deleteTarget = { path: '__bulk__', name: `${selectedCount} items` };
showDeleteConfirm = true;
@ -1370,6 +1389,7 @@
bind:saving
bind:currentSlide
{selectedFile}
readOnly={!selectedFileWritable}
{fileLoading}
{fileImageUrl}
{fileVideoUrl}
@ -1394,7 +1414,7 @@
overlay={overlay || isDraggingHandle}
onSave={async (content) => {
const terminal = selectedTerminal;
if (!terminal || !selectedFile) return;
if (!terminal || !selectedFile || !selectedFileWritable) return;
const fileName = selectedFile.split('/').pop() ?? 'file';
const dir = selectedFile.substring(0, selectedFile.lastIndexOf('/') + 1) || '/';
const file = new File([content], fileName, { type: 'text/plain' });
@ -1490,6 +1510,7 @@
onSelect={handleSelect}
onLongPress={enterSelectionMode}
showDate={sortBy === 'date'}
parentWritable={currentWritable}
/>
{/each}
</ul>

View file

@ -7,6 +7,7 @@
export let count: number = 0;
export let hasFiles: boolean = false;
export let canDelete = true;
export let onDelete: () => void = () => {};
export let onDownload: () => void = () => {};
@ -64,8 +65,9 @@
<Tooltip content={$i18n.t('Delete')}>
<button
class="p-1 rounded transition text-gray-400 dark:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-gray-600 dark:hover:text-gray-400"
class="p-1 rounded transition text-gray-400 dark:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-gray-600 dark:hover:text-gray-400 disabled:opacity-30 disabled:hover:bg-transparent"
on:click={onDelete}
disabled={!canDelete}
aria-label={$i18n.t('Delete')}
>
<GarbageBin className="size-3.5" />

View file

@ -32,6 +32,10 @@
export let onSelect: (entry: FileEntry, event: MouseEvent) => void = () => {};
export let onLongPress: () => void = () => {};
export let showDate: boolean = false;
export let parentWritable = true;
$: writable = entry.writable !== false;
$: canMutate = parentWritable && writable;
const formatRelativeTime = (epoch: number): string => {
const diff = Math.floor(Date.now() / 1000) - epoch;
@ -143,6 +147,7 @@
role={entry.type === 'directory' ? 'button' : undefined}
on:dragover={(e) => {
if (entry.type !== 'directory') return;
if (!writable) return;
if (!e.dataTransfer?.types.includes('application/x-terminal-file-move')) return;
e.preventDefault();
e.stopPropagation();
@ -155,6 +160,7 @@
}}
on:drop={(e) => {
if (entry.type !== 'directory') return;
if (!writable) return;
const raw = e.dataTransfer?.getData('application/x-terminal-file-move');
if (!raw) return;
e.preventDefault();
@ -173,8 +179,12 @@
>
<button
class="flex-1 flex items-center gap-2 px-3 py-1.5 text-left min-w-0"
draggable={true}
draggable={canMutate}
on:dragstart={(e) => {
if (!canMutate) {
e.preventDefault();
return;
}
const filePath = `${currentPath}${entry.name}`;
// If dragging a selected item, drag all selected
if (selected && selectedPaths.size > 1) {
@ -282,6 +292,9 @@
{entry.name}
</span>
{/if}
{#if !writable && !renaming}
<span class="text-[10px] text-gray-400 shrink-0">Read-only</span>
{/if}
{#if entry.type === 'file' && entry.size !== undefined && !renaming}
{#if showDate && entry.modified}
<span class="text-[10px] text-gray-400 shrink-0"
@ -354,9 +367,11 @@
<button
type="button"
class="select-none flex h-[1.6875rem] w-full items-center gap-2 rounded-xl px-2 text-[13px] hover:bg-gray-50/40 dark:hover:bg-gray-800/40 transition"
class="select-none flex h-[1.6875rem] w-full items-center gap-2 rounded-xl px-2 text-[13px] hover:bg-gray-50/40 dark:hover:bg-gray-800/40 transition disabled:opacity-40 disabled:hover:bg-transparent"
disabled={!canMutate}
on:click={(e) => {
e.stopPropagation();
if (!canMutate) return;
startRename();
}}
>
@ -366,9 +381,11 @@
<button
type="button"
class="select-none flex h-[1.6875rem] w-full items-center gap-2 rounded-xl px-2 text-[13px] hover:bg-gray-50/40 dark:hover:bg-gray-800/40 transition"
class="select-none flex h-[1.6875rem] w-full items-center gap-2 rounded-xl px-2 text-[13px] hover:bg-gray-50/40 dark:hover:bg-gray-800/40 transition disabled:opacity-40 disabled:hover:bg-transparent"
disabled={!canMutate}
on:click={(e) => {
e.stopPropagation();
if (!canMutate) return;
onDelete(`${currentPath}${entry.name}`, entry.name);
}}
>

View file

@ -14,6 +14,7 @@
export let breadcrumbs: { label: string; path: string }[] = [];
export let selectedFile: string | null = null;
export let loading = false;
export let writable = true;
export let onNavigate: (path: string) => void = () => {};
export let onRefresh: () => void = () => {};
@ -114,6 +115,7 @@
: ''}"
on:click={() => onNavigate(crumb.path)}
on:dragover={(e) => {
if (!writable) return;
if (!e.dataTransfer?.types.includes('application/x-terminal-file-move')) return;
e.preventDefault();
e.stopPropagation();
@ -123,6 +125,7 @@
if (dragOverCrumb === i) dragOverCrumb = null;
}}
on:drop={(e) => {
if (!writable) return;
const raw = e.dataTransfer?.getData('application/x-terminal-file-move');
if (!raw) return;
e.preventDefault();
@ -145,6 +148,11 @@
</span>
{/if}
</div>
{#if !writable}
<span class="text-[10px] text-gray-400 dark:text-gray-500 shrink-0">
Read-only
</span>
{/if}
<Tooltip content={$i18n.t('Refresh')}>
<button
@ -240,8 +248,9 @@
</Dropdown>
<Tooltip content={$i18n.t('New Folder')}>
<button
class="shrink-0 p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-400"
class="shrink-0 p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-400 disabled:opacity-30 disabled:hover:bg-transparent"
on:click={onNewFolder}
disabled={!writable}
aria-label={$i18n.t('New Folder')}
>
<NewFolderAlt className="size-3.5" />
@ -249,8 +258,9 @@
</Tooltip>
<Tooltip content={$i18n.t('New File')}>
<button
class="shrink-0 p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-400"
class="shrink-0 p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-400 disabled:opacity-30 disabled:hover:bg-transparent"
on:click={onNewFile}
disabled={!writable}
aria-label={$i18n.t('New File')}
>
<FilePlusAlt className="size-3.5" />
@ -279,8 +289,9 @@
</Tooltip>
<Tooltip content={$i18n.t('Upload')}>
<button
class="shrink-0 p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-400"
class="shrink-0 p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-400 disabled:opacity-30 disabled:hover:bg-transparent"
on:click={() => uploadInput?.click()}
disabled={!writable}
aria-label={$i18n.t('Upload')}
>
<svg
@ -305,7 +316,7 @@
multiple
hidden
on:change={async () => {
if (!uploadInput?.files?.length) return;
if (!writable || !uploadInput?.files?.length) return;
onUploadFiles(Array.from(uploadInput.files));
uploadInput.value = '';
}}

View file

@ -41,6 +41,7 @@
export let onSheetChange: ((sheet: string) => void) | null = null;
export let overlay = false;
export let readOnly = false;
export let onSave: ((content: string) => Promise<void>) | null = null;
@ -59,6 +60,7 @@
};
export const startEdit = async () => {
if (readOnly) return;
editContent = fileContent ?? '';
editing = true;
showRaw = true;
@ -67,7 +69,7 @@
};
export const saveEdit = async () => {
if (!onSave) return;
if (!onSave || readOnly) return;
saving = true;
await onSave(editContent);
saving = false;
@ -81,7 +83,7 @@
/** Save code file directly from CodeMirror */
export const saveCodeFile = async () => {
if (!onSave) return;
if (!onSave || readOnly) return;
saving = true;
const content = fileCodeEditorRef?.getValue() ?? '';
await onSave(content);
@ -426,7 +428,7 @@
bind:this={fileCodeEditorRef}
value={fileContent ?? ''}
filePath={selectedFile}
{onSave}
onSave={readOnly ? null : onSave}
/>
</div>
{:else if isMarkdown && !showRaw}
@ -439,7 +441,7 @@
bind:this={fileCodeEditorRef}
value={fileContent ?? ''}
filePath={selectedFile}
{onSave}
onSave={readOnly ? null : onSave}
/>
</div>
{:else if isCsv && !showRaw && csvRows.length > 0}
@ -496,7 +498,7 @@
bind:this={fileCodeEditorRef}
value={fileContent ?? ''}
filePath={selectedFile}
{onSave}
onSave={readOnly ? null : onSave}
/>
</div>
{:else if isSvg && highlightedHtml && !showRaw}