Show toast when Python script saves output files to project

GitOrigin-RevId: 9ca5201645953f86c3ac8e83f545dfbcdac2b35c
This commit is contained in:
Domagoj Kriskovic
2026-05-22 08:05:55 +00:00
committed by Copybot
parent 014ac37704
commit 803ba735ca
8 changed files with 217 additions and 1 deletions
@@ -1,5 +1,6 @@
import { useMemo, useSyncExternalStore } from 'react'
import { useTranslation } from 'react-i18next'
import classNames from 'classnames'
import OLButton from '@/shared/components/ol/ol-button'
import OLButtonToolbar from '@/shared/components/ol/ol-button-toolbar'
import MaterialIcon from '@/shared/components/material-icon'
@@ -33,7 +34,14 @@ export default function PythonOutputPane() {
<div className="ide-redesign-python-output-pane">
<OLButtonToolbar className="ide-redesign-python-output-pane-toolbar">
<div className="ide-redesign-python-output-pane-toolbar-left">
<div className="ide-redesign-python-output-pane-run-button-wrapper">
<div
className={classNames(
'ide-redesign-python-output-pane-run-button-wrapper',
{
'compile-button-group-running': status === 'running',
}
)}
>
<OLButton
onClick={() => {
if (status === 'running') {
@@ -0,0 +1,48 @@
import { GlobalToastGeneratorEntry } from '@/features/ide-react/components/global-toasts'
import { useTranslation } from 'react-i18next'
const stripProjectPrefix = (path: string) => path.replace(/^\/project\/?/, '')
const PythonFilesSavedToast = ({ paths }: { paths: string[] }) => {
const { t } = useTranslation()
if (paths.length === 1) {
return (
<span>
{t('x_saved_to_your_project', {
fileName: stripProjectPrefix(paths[0]),
})}
</span>
)
}
return (
<span>{t('x_files_saved_to_your_project', { count: paths.length })}</span>
)
}
const isStringArray = (value: unknown): value is string[] =>
Array.isArray(value) && value.every(v => typeof v === 'string')
const generators: GlobalToastGeneratorEntry[] = [
{
key: 'python:files-saved',
generator: ({ paths }) => ({
content: (
<PythonFilesSavedToast paths={isStringArray(paths) ? paths : []} />
),
type: 'success',
autoHide: true,
delay: 5000,
isDismissible: true,
}),
},
]
export default generators
export const showPythonFilesSavedToast = (paths: string[]) => {
window.dispatchEvent(
new CustomEvent('ide:show-toast', {
detail: { key: 'python:files-saved', paths },
})
)
}
@@ -6,6 +6,7 @@ import { v4 as uuid } from 'uuid'
import { debugConsole } from '@/utils/debugging'
import { sendMB } from '@/infrastructure/event-tracking'
import { PyodideWorkerClient } from './pyodide-worker-client'
import { showPythonFilesSavedToast } from './python-output-toasts'
import type { OutputStream } from './pyodide-worker-messages'
import type {
BatchUploadItem,
@@ -15,6 +16,11 @@ import type {
export type FileUploader = (items: BatchUploadItem[]) => Promise<UploadResult[]>
const MAX_OUTPUT_LINES = 100
const PROJECT_FS_PREFIX = '/project/'
function stripProjectFsPrefix(p: string): string {
return p.startsWith(PROJECT_FS_PREFIX) ? p.slice(PROJECT_FS_PREFIX.length) : p
}
export type ExecutionStatus =
| 'loading'
@@ -145,6 +151,17 @@ export class PythonRunner {
filesWrittenExtensions: collectExtensions(event.outputs),
})
// event.outputs are full worker paths (/project/foo.txt) while
// event.failedUploads are relativePaths (foo.txt); strip the
// prefix before comparing.
const failed = new Set(event.failedUploads)
const uploadedPaths = event.outputs
.map(stripProjectFsPrefix)
.filter(p => !failed.has(p))
if (uploadedPaths.length > 0) {
showPythonFilesSavedToast(uploadedPaths)
}
this.updateState({ status: 'finished' })
}
}
@@ -8,6 +8,7 @@ import { OLToastContainer } from '@/shared/components/ol/ol-toast-container'
import clipboardToastGenerators from '@/features/source-editor/components/clipboard-toasts'
import importDocumentFeedbackToastGenerators from '@/features/project-list/components/new-project-button/import-document-feedback-toast'
import exportDocumentToastGenerators from '@/features/ide-react/components/toolbar/export-document-toasts'
import pythonOutputToastGenerators from '@/features/ide-react/components/editor/python/python-output-toasts'
const moduleGeneratorsImport = importOverleafModules('toastGenerators') as {
import: { default: GlobalToastGeneratorEntry[] }
@@ -31,6 +32,7 @@ const GENERATOR_LIST: GlobalToastGeneratorEntry[] = [
...clipboardToastGenerators,
...importDocumentFeedbackToastGenerators,
...exportDocumentToastGenerators,
...pythonOutputToastGenerators,
]
const GENERATOR_MAP: Map<string, GlobalToastGenerator> = new Map(
GENERATOR_LIST.map(({ key, generator }) => [key, generator])