u
This commit is contained in:
@@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
import { Automat } from 'react-automat';
|
import { Automat } from 'react-automat';
|
||||||
import { vfs as vfsApi } from '../api';
|
import { vfs as vfsApi } from '../api';
|
||||||
|
import { isAppletFile, parseAppletSchemas } from '../components/vfs/vfsUtils';
|
||||||
import authAutomat from './authAutomat';
|
import authAutomat from './authAutomat';
|
||||||
import sseAutomat from './sseAutomat';
|
import sseAutomat from './sseAutomat';
|
||||||
|
|
||||||
@@ -25,6 +26,7 @@ import sseAutomat from './sseAutomat';
|
|||||||
* @property {boolean} isText Whether the file can be edited in the text editor
|
* @property {boolean} isText Whether the file can be edited in the text editor
|
||||||
* @property {boolean} isImage Whether the file can be rendered as an image
|
* @property {boolean} isImage Whether the file can be rendered as an image
|
||||||
* @property {boolean} isPdf Whether the file is a PDF
|
* @property {boolean} isPdf Whether the file is a PDF
|
||||||
|
* @property {string} [description] Applet description declared in the @applet-description header
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -71,6 +73,7 @@ const initialState = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let vfsAutomat;
|
let vfsAutomat;
|
||||||
|
const appletDescriptionCache = new Map();
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
/**
|
/**
|
||||||
@@ -120,7 +123,17 @@ const actions = {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const res = await vfsApi.list();
|
const res = await vfsApi.list();
|
||||||
const files = Array.isArray(res?.files) ? res.files : [];
|
const rawFiles = Array.isArray(res?.files) ? res.files : [];
|
||||||
|
const files = rawFiles.map((f) => {
|
||||||
|
if (f.description) {
|
||||||
|
appletDescriptionCache.set(f.name, f.description);
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
if (appletDescriptionCache.has(f.name)) {
|
||||||
|
return { ...f, description: appletDescriptionCache.get(f.name) };
|
||||||
|
}
|
||||||
|
return f;
|
||||||
|
});
|
||||||
const currentSelected = vfsAutomat.state.selectedFileName;
|
const currentSelected = vfsAutomat.state.selectedFileName;
|
||||||
const updatedSelectedFile = currentSelected
|
const updatedSelectedFile = currentSelected
|
||||||
? files.find((f) => f.name === currentSelected) || null
|
? files.find((f) => f.name === currentSelected) || null
|
||||||
@@ -132,6 +145,10 @@ const actions = {
|
|||||||
loadingFiles: false,
|
loadingFiles: false,
|
||||||
loadError: null,
|
loadError: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Asynchronously fetch descriptions for any applet files that don't have one yet
|
||||||
|
void actions.loadAppletDescriptions(files);
|
||||||
|
|
||||||
return files;
|
return files;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[vfsAutomat] loadFiles failed:', err);
|
console.error('[vfsAutomat] loadFiles failed:', err);
|
||||||
@@ -143,6 +160,68 @@ const actions = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asynchronously load description headers for applets missing cached descriptions.
|
||||||
|
* @param {VfsFile[]} [files]
|
||||||
|
*/
|
||||||
|
async loadAppletDescriptions(files) {
|
||||||
|
const list = files || vfsAutomat.state.files;
|
||||||
|
const missing = list.filter((f) => isAppletFile(f.name) && !f.description);
|
||||||
|
if (missing.length === 0) return;
|
||||||
|
|
||||||
|
let changed = false;
|
||||||
|
await Promise.all(
|
||||||
|
missing.map(async (file) => {
|
||||||
|
try {
|
||||||
|
const text = await vfsApi.getText(file.name);
|
||||||
|
const { description } = parseAppletSchemas(text);
|
||||||
|
if (description) {
|
||||||
|
appletDescriptionCache.set(file.name, description);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore failures reading unreadable files
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
const nextFiles = vfsAutomat.state.files.map((f) => {
|
||||||
|
const desc = appletDescriptionCache.get(f.name);
|
||||||
|
return desc && f.description !== desc ? { ...f, description: desc } : f;
|
||||||
|
});
|
||||||
|
const currentSelected = vfsAutomat.state.selectedFileName;
|
||||||
|
const updatedSelectedFile = currentSelected
|
||||||
|
? nextFiles.find((f) => f.name === currentSelected) || null
|
||||||
|
: null;
|
||||||
|
vfsAutomat.setState({
|
||||||
|
files: nextFiles,
|
||||||
|
selectedFile: updatedSelectedFile,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manually record or update an applet file's description in the state & cache.
|
||||||
|
* @param {string} fileName
|
||||||
|
* @param {string} description
|
||||||
|
*/
|
||||||
|
setAppletDescription(fileName, description) {
|
||||||
|
if (!fileName || !description) return;
|
||||||
|
appletDescriptionCache.set(fileName, description);
|
||||||
|
const nextFiles = vfsAutomat.state.files.map((f) =>
|
||||||
|
f.name === fileName ? { ...f, description } : f
|
||||||
|
);
|
||||||
|
const updatedSelectedFile =
|
||||||
|
vfsAutomat.state.selectedFileName === fileName
|
||||||
|
? { ...(vfsAutomat.state.selectedFile || {}), description }
|
||||||
|
: vfsAutomat.state.selectedFile;
|
||||||
|
vfsAutomat.setState({
|
||||||
|
files: nextFiles,
|
||||||
|
selectedFile: updatedSelectedFile,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Select a file for viewing or editing.
|
* Select a file for viewing or editing.
|
||||||
* @param {string|null} fileName
|
* @param {string|null} fileName
|
||||||
@@ -179,9 +258,18 @@ const actions = {
|
|||||||
const text = await vfsApi.getText(fileName);
|
const text = await vfsApi.getText(fileName);
|
||||||
// Only apply if still selected
|
// Only apply if still selected
|
||||||
if (vfsAutomat.state.selectedFileName === fileName) {
|
if (vfsAutomat.state.selectedFileName === fileName) {
|
||||||
|
let updatedFile = file;
|
||||||
|
if (isAppletFile(fileName)) {
|
||||||
|
const { description } = parseAppletSchemas(text);
|
||||||
|
if (description) {
|
||||||
|
appletDescriptionCache.set(fileName, description);
|
||||||
|
updatedFile = { ...file, description };
|
||||||
|
}
|
||||||
|
}
|
||||||
vfsAutomat.setState({
|
vfsAutomat.setState({
|
||||||
fileContent: text,
|
fileContent: text,
|
||||||
originalContent: text,
|
originalContent: text,
|
||||||
|
selectedFile: updatedFile,
|
||||||
isDirty: false,
|
isDirty: false,
|
||||||
loadingContent: false,
|
loadingContent: false,
|
||||||
});
|
});
|
||||||
@@ -224,6 +312,14 @@ const actions = {
|
|||||||
vfsAutomat.setState({ isSaving: true, saveError: null });
|
vfsAutomat.setState({ isSaving: true, saveError: null });
|
||||||
try {
|
try {
|
||||||
await vfsApi.save(selectedFileName, fileContent, selectedFile?.mime);
|
await vfsApi.save(selectedFileName, fileContent, selectedFile?.mime);
|
||||||
|
if (isAppletFile(selectedFileName)) {
|
||||||
|
const { description } = parseAppletSchemas(fileContent);
|
||||||
|
if (description) {
|
||||||
|
appletDescriptionCache.set(selectedFileName, description);
|
||||||
|
} else {
|
||||||
|
appletDescriptionCache.delete(selectedFileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
vfsAutomat.setState({
|
vfsAutomat.setState({
|
||||||
originalContent: fileContent,
|
originalContent: fileContent,
|
||||||
isDirty: false,
|
isDirty: false,
|
||||||
@@ -274,6 +370,7 @@ const actions = {
|
|||||||
if (!fileName) return;
|
if (!fileName) return;
|
||||||
try {
|
try {
|
||||||
await vfsApi.delete(fileName);
|
await vfsApi.delete(fileName);
|
||||||
|
appletDescriptionCache.delete(fileName);
|
||||||
if (vfsAutomat.state.selectedFileName === fileName) {
|
if (vfsAutomat.state.selectedFileName === fileName) {
|
||||||
actions.selectFile(null);
|
actions.selectFile(null);
|
||||||
}
|
}
|
||||||
@@ -320,6 +417,11 @@ const actions = {
|
|||||||
if (!cleanOld || !cleanNew) throw new Error('Alter und neuer Pfad dürfen nicht leer sein');
|
if (!cleanOld || !cleanNew) throw new Error('Alter und neuer Pfad dürfen nicht leer sein');
|
||||||
try {
|
try {
|
||||||
const res = await vfsApi.rename(cleanOld, cleanNew);
|
const res = await vfsApi.rename(cleanOld, cleanNew);
|
||||||
|
if (appletDescriptionCache.has(cleanOld)) {
|
||||||
|
const desc = appletDescriptionCache.get(cleanOld);
|
||||||
|
appletDescriptionCache.delete(cleanOld);
|
||||||
|
appletDescriptionCache.set(cleanNew, desc);
|
||||||
|
}
|
||||||
const isCurrentSelected = vfsAutomat.state.selectedFileName === cleanOld;
|
const isCurrentSelected = vfsAutomat.state.selectedFileName === cleanOld;
|
||||||
await actions.loadFiles(true);
|
await actions.loadFiles(true);
|
||||||
if (isCurrentSelected) {
|
if (isCurrentSelected) {
|
||||||
@@ -379,6 +481,7 @@ const actions = {
|
|||||||
* Reset state (on logout or company switch).
|
* Reset state (on logout or company switch).
|
||||||
*/
|
*/
|
||||||
reset() {
|
reset() {
|
||||||
|
appletDescriptionCache.clear();
|
||||||
vfsAutomat.setState({
|
vfsAutomat.setState({
|
||||||
files: [],
|
files: [],
|
||||||
loadingFiles: false,
|
loadingFiles: false,
|
||||||
|
|||||||
@@ -31,10 +31,11 @@ import { vfs as vfsApi } from '../../api';
|
|||||||
export default class VfsAppletRunner extends PureComponent {
|
export default class VfsAppletRunner extends PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
const { inputSchema, outputColumns } = parseAppletSchemas(props.content || '');
|
const { inputSchema, outputColumns, description } = parseAppletSchemas(props.content || '');
|
||||||
this.state = {
|
this.state = {
|
||||||
inputSchema,
|
inputSchema,
|
||||||
outputColumns,
|
outputColumns,
|
||||||
|
description: description || props.file?.description || '',
|
||||||
inputValues: buildAppletDefaults(inputSchema),
|
inputValues: buildAppletDefaults(inputSchema),
|
||||||
isRunning: false,
|
isRunning: false,
|
||||||
runError: null,
|
runError: null,
|
||||||
@@ -50,10 +51,11 @@ export default class VfsAppletRunner extends PureComponent {
|
|||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
if (prevProps.content !== this.props.content || prevProps.fileName !== this.props.fileName) {
|
if (prevProps.content !== this.props.content || prevProps.fileName !== this.props.fileName) {
|
||||||
const { inputSchema, outputColumns } = parseAppletSchemas(this.props.content || '');
|
const { inputSchema, outputColumns, description } = parseAppletSchemas(this.props.content || '');
|
||||||
this.setState({
|
this.setState({
|
||||||
inputSchema,
|
inputSchema,
|
||||||
outputColumns,
|
outputColumns,
|
||||||
|
description: description || this.props.file?.description || '',
|
||||||
inputValues: buildAppletDefaults(inputSchema),
|
inputValues: buildAppletDefaults(inputSchema),
|
||||||
isRunning: false,
|
isRunning: false,
|
||||||
runError: null,
|
runError: null,
|
||||||
@@ -122,6 +124,7 @@ export default class VfsAppletRunner extends PureComponent {
|
|||||||
stdout: res?.stdout || '',
|
stdout: res?.stdout || '',
|
||||||
durationMs: res?.durationMs ?? null,
|
durationMs: res?.durationMs ?? null,
|
||||||
truncated: Boolean(res?.truncated),
|
truncated: Boolean(res?.truncated),
|
||||||
|
description: res?.description || this.state.description,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -236,6 +239,7 @@ export default class VfsAppletRunner extends PureComponent {
|
|||||||
const {
|
const {
|
||||||
inputSchema,
|
inputSchema,
|
||||||
outputColumns,
|
outputColumns,
|
||||||
|
description: stateDescription,
|
||||||
inputValues,
|
inputValues,
|
||||||
isRunning,
|
isRunning,
|
||||||
runError,
|
runError,
|
||||||
@@ -245,6 +249,7 @@ export default class VfsAppletRunner extends PureComponent {
|
|||||||
durationMs,
|
durationMs,
|
||||||
truncated,
|
truncated,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
const description = stateDescription || file?.description;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
|
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
|
||||||
@@ -268,14 +273,27 @@ export default class VfsAppletRunner extends PureComponent {
|
|||||||
<ArrowBackRoundedIcon fontSize="small" />
|
<ArrowBackRoundedIcon fontSize="small" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<VfsFileIcon fileName={fileName} mime={file?.mime} fontSize="small" />
|
<VfsFileIcon fileName={fileName} mime={file?.mime} fontSize="small" />
|
||||||
<Typography
|
<Box sx={{ minWidth: 0 }}>
|
||||||
variant="body2"
|
<Typography
|
||||||
fontWeight={700}
|
variant="body2"
|
||||||
noWrap
|
fontWeight={700}
|
||||||
sx={{ fontFamily: 'monospace', fontSize: '0.85rem' }}
|
noWrap
|
||||||
>
|
sx={{ fontFamily: 'monospace', fontSize: '0.85rem' }}
|
||||||
{fileName}
|
>
|
||||||
</Typography>
|
{fileName}
|
||||||
|
</Typography>
|
||||||
|
{description && (
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
color="text.secondary"
|
||||||
|
noWrap
|
||||||
|
title={description}
|
||||||
|
sx={{ display: 'block', fontSize: '0.72rem', lineHeight: 1.2 }}
|
||||||
|
>
|
||||||
|
{description}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, flexShrink: 0 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, flexShrink: 0 }}>
|
||||||
|
|||||||
@@ -55,7 +55,11 @@ function buildFileTree(files, searchQuery = '') {
|
|||||||
const q = searchQuery ? searchQuery.trim().toLowerCase() : '';
|
const q = searchQuery ? searchQuery.trim().toLowerCase() : '';
|
||||||
const filtered = (files || []).filter((f) => {
|
const filtered = (files || []).filter((f) => {
|
||||||
if (!q) return true;
|
if (!q) return true;
|
||||||
return f.name.toLowerCase().includes(q) || (f.mime && f.mime.toLowerCase().includes(q));
|
return (
|
||||||
|
f.name.toLowerCase().includes(q) ||
|
||||||
|
(f.mime && f.mime.toLowerCase().includes(q)) ||
|
||||||
|
(f.description && f.description.toLowerCase().includes(q))
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const root = {
|
const root = {
|
||||||
@@ -457,25 +461,65 @@ export default class VfsFileExplorer extends PureComponent {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Box sx={{ minWidth: 0, flexGrow: 1, pr: 1 }}>
|
<Box sx={{ minWidth: 0, flexGrow: 1, pr: 1 }}>
|
||||||
<Typography
|
{file.fileData?.description ? (
|
||||||
variant="body2"
|
<>
|
||||||
fontWeight={isSelected ? 600 : 500}
|
{/* Line 1: Description */}
|
||||||
noWrap
|
<Typography
|
||||||
sx={{ fontFamily: 'monospace', fontSize: '0.82rem' }}
|
variant="body2"
|
||||||
>
|
fontWeight={isSelected ? 600 : 500}
|
||||||
{file.name}
|
noWrap
|
||||||
</Typography>
|
title={file.fileData.description}
|
||||||
|
sx={{ fontSize: '0.82rem', lineHeight: 1.3 }}
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.25, mt: 0.15 }}>
|
>
|
||||||
<Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.7rem' }}>
|
{file.fileData.description}
|
||||||
{formatVfsSize(file.fileData.size)}
|
|
||||||
</Typography>
|
|
||||||
{file.fileData.updated_at && (
|
|
||||||
<Typography variant="caption" color="text.disabled" sx={{ fontSize: '0.68rem' }}>
|
|
||||||
{formatVfsDate(file.fileData.updated_at)}
|
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
|
||||||
</Box>
|
{/* Line 2: size, date, filename */}
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mt: 0.15, minWidth: 0 }}>
|
||||||
|
<Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.7rem', flexShrink: 0 }}>
|
||||||
|
{formatVfsSize(file.fileData.size)}
|
||||||
|
</Typography>
|
||||||
|
{file.fileData.updated_at && (
|
||||||
|
<Typography variant="caption" color="text.disabled" sx={{ fontSize: '0.68rem', flexShrink: 0 }}>
|
||||||
|
{formatVfsDate(file.fileData.updated_at)}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
color="text.secondary"
|
||||||
|
noWrap
|
||||||
|
title={file.name}
|
||||||
|
sx={{ fontFamily: 'monospace', fontSize: '0.68rem', minWidth: 0 }}
|
||||||
|
>
|
||||||
|
{file.name}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* Line 1: filename */}
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
fontWeight={isSelected ? 600 : 500}
|
||||||
|
noWrap
|
||||||
|
sx={{ fontFamily: 'monospace', fontSize: '0.82rem' }}
|
||||||
|
>
|
||||||
|
{file.name}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{/* Line 2: size, date */}
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.25, mt: 0.15 }}>
|
||||||
|
<Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.7rem' }}>
|
||||||
|
{formatVfsSize(file.fileData.size)}
|
||||||
|
</Typography>
|
||||||
|
{file.fileData.updated_at && (
|
||||||
|
<Typography variant="caption" color="text.disabled" sx={{ fontSize: '0.68rem' }}>
|
||||||
|
{formatVfsDate(file.fileData.updated_at)}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Quick action buttons on hover / select */}
|
{/* Quick action buttons on hover / select */}
|
||||||
|
|||||||
@@ -208,15 +208,38 @@ export function parseCsvRows(text, separator = APPLET_CSV_SEPARATOR) {
|
|||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the applet description header.
|
||||||
|
*
|
||||||
|
* Accepts either a quoted string (e.g. `"Lorem ipsum"` or `'Lorem ipsum'`) or bare text.
|
||||||
|
* Strips surrounding quotes if present and trims whitespace.
|
||||||
|
*
|
||||||
|
* @param {string} header Raw text after the `// @applet-description ` marker.
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function parseAppletDescription(header) {
|
||||||
|
const text = String(header || '').trim();
|
||||||
|
if (!text) return '';
|
||||||
|
if ((text.startsWith('"') && text.endsWith('"')) || (text.startsWith("'") && text.endsWith("'"))) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(text);
|
||||||
|
if (typeof parsed === 'string' && parsed.trim()) return parsed.trim();
|
||||||
|
} catch {
|
||||||
|
const unquoted = text.slice(1, -1).trim();
|
||||||
|
if (unquoted) return unquoted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse applet metadata from the file header.
|
* Parse applet metadata from the file header.
|
||||||
*
|
*
|
||||||
* Mirrors the backend's `readApplet()`: the input JSON Schema must be on line 1 and the typed
|
* Mirrors the backend's `readApplet()`: the input JSON Schema must be on line 1, the typed
|
||||||
* CSV output columns on line 2. Parsing the same two lines keeps the UI in lockstep with what
|
* CSV output columns on line 2, and the applet description on line 3.
|
||||||
* the sandbox will actually accept.
|
|
||||||
*
|
*
|
||||||
* @param {string} content Applet file source.
|
* @param {string} content Applet file source.
|
||||||
* @returns {{ inputSchema: object|null, outputColumns: Array<{name: string, type: string}>|null }}
|
* @returns {{ inputSchema: object|null, outputColumns: Array<{name: string, type: string}>|null, description: string }}
|
||||||
*/
|
*/
|
||||||
export function parseAppletSchemas(content) {
|
export function parseAppletSchemas(content) {
|
||||||
const lines = String(content || '').replace(/^\uFEFF/, '').split(/\r?\n/);
|
const lines = String(content || '').replace(/^\uFEFF/, '').split(/\r?\n/);
|
||||||
@@ -238,7 +261,15 @@ export function parseAppletSchemas(content) {
|
|||||||
outputColumns = parseTypedCsvColumns(outputMatch[1]);
|
outputColumns = parseTypedCsvColumns(outputMatch[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { inputSchema, outputColumns };
|
let description = '';
|
||||||
|
const descMatch =
|
||||||
|
lines[2]?.match(/^\/\/ @applet-description (.+)$/) ||
|
||||||
|
lines.slice(0, 10).map((l) => l.match(/^\/\/ @applet-description (.+)$/)).find(Boolean);
|
||||||
|
if (descMatch) {
|
||||||
|
description = parseAppletDescription(descMatch[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { inputSchema, outputColumns, description };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user