This commit is contained in:
seb
2026-10-12 14:30:26 +02:00
parent 627868eb2e
commit 15dce3c1cf
4 changed files with 231 additions and 35 deletions

View File

@@ -12,6 +12,7 @@
import { Automat } from 'react-automat';
import { vfs as vfsApi } from '../api';
import { isAppletFile, parseAppletSchemas } from '../components/vfs/vfsUtils';
import authAutomat from './authAutomat';
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} isImage Whether the file can be rendered as an image
* @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;
const appletDescriptionCache = new Map();
const actions = {
/**
@@ -120,7 +123,17 @@ const actions = {
}
try {
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 updatedSelectedFile = currentSelected
? files.find((f) => f.name === currentSelected) || null
@@ -132,6 +145,10 @@ const actions = {
loadingFiles: false,
loadError: null,
});
// Asynchronously fetch descriptions for any applet files that don't have one yet
void actions.loadAppletDescriptions(files);
return files;
} catch (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.
* @param {string|null} fileName
@@ -179,9 +258,18 @@ const actions = {
const text = await vfsApi.getText(fileName);
// Only apply if still selected
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({
fileContent: text,
originalContent: text,
selectedFile: updatedFile,
isDirty: false,
loadingContent: false,
});
@@ -224,6 +312,14 @@ const actions = {
vfsAutomat.setState({ isSaving: true, saveError: null });
try {
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({
originalContent: fileContent,
isDirty: false,
@@ -274,6 +370,7 @@ const actions = {
if (!fileName) return;
try {
await vfsApi.delete(fileName);
appletDescriptionCache.delete(fileName);
if (vfsAutomat.state.selectedFileName === fileName) {
actions.selectFile(null);
}
@@ -320,6 +417,11 @@ const actions = {
if (!cleanOld || !cleanNew) throw new Error('Alter und neuer Pfad dürfen nicht leer sein');
try {
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;
await actions.loadFiles(true);
if (isCurrentSelected) {
@@ -379,6 +481,7 @@ const actions = {
* Reset state (on logout or company switch).
*/
reset() {
appletDescriptionCache.clear();
vfsAutomat.setState({
files: [],
loadingFiles: false,