@statelyjs/files
v0.5.0
Published
File management plugin for Stately
Downloads
317
Readme
@statelyjs/files
File system integration plugin for Stately.
Provides file browsing, versioned file management, uploads, downloads, and relative path handling.
Overview
This package connects your application to a stately-files backend (Rust), giving you:
- File Manager page - Browse files across
cache,data, anduploaddirectories - Versioned file support - View version history and download specific versions
- Upload/download - Seamless file operations with progress feedback
- RelativePath field - Custom form field for selecting files from your backend
Installation
pnpm add @statelyjs/filesQuick Start
1. Add Schema Plugin
import { stately } from '@statelyjs/stately/schema';
import { type FilesPlugin, filesPlugin } from '@statelyjs/files';
const schema = stately<MySchemas, readonly [FilesPlugin]>(openapiDoc, PARSED_SCHEMAS)
.withPlugin(filesPlugin());2. Add UI Plugin
import { statelyUi } from '@statelyjs/stately';
import { type FilesUiPlugin, filesUiPlugin } from '@statelyjs/files';
const runtime = statelyUi<MySchemas, readonly [FilesUiPlugin]>({
schema,
client,
core: { api: { pathPrefix: '/entity' } },
options: { api: { pathPrefix: '/api' } },
}).withPlugin(filesUiPlugin({
api: { pathPrefix: '/files' },
}));3. Wrap Your App
import { QueryClientProvider } from '@tanstack/react-query';
import { AppStatelyProvider, runtime } from './lib/stately';
function App() {
return (
<QueryClientProvider client={queryClient}>
<AppStatelyProvider runtime={runtime}>
<YourApp />
</AppStatelyProvider>
</QueryClientProvider>
);
}4. Configure Styles
Import the files styles and ensure your @source covers @statelyjs packages:
@import "tailwindcss";
@import "@statelyjs/stately/styles.css";
@import "@statelyjs/files/styles.css";
@source "./node_modules/@statelyjs";Adding Routes
The plugin provides a FileManagerPage page component. Add it to your router:
import { FileManagerPage } from '@statelyjs/files/pages';
// Example with React Router
<Route path="/files" element={<FileManagerPage />} />Hooks
All hooks use the files API through the plugin context:
useFileExplore
Browse files and directories:
import { useFileExplore } from '@statelyjs/files/hooks';
function FileBrowser() {
const { data, isLoading } = useFileExplore({ path: 'data/configs' });
return (
<ul>
{data?.entries.map(entry => (
<li key={entry.name}>{entry.name}</li>
))}
</ul>
);
}useFileVersions
Get version history for a file:
import { useFileVersions } from '@statelyjs/files/hooks';
const { data: versions } = useFileVersions({
target: 'data',
path: 'configs/settings.json',
});useDownload
Download files (supports versioned downloads):
import { useDownload } from '@statelyjs/files/hooks';
function DownloadButton({ path }: { path: string }) {
const { mutate: download, isPending } = useDownload();
return (
<button
onClick={() => download({ target: 'data', path })}
disabled={isPending}
>
{isPending ? 'Downloading...' : 'Download'}
</button>
);
}useUpload
Upload files:
import { useUpload } from '@statelyjs/files/hooks';
const { mutate: upload, isPending } = useUpload({
onSuccess: () => console.log('Upload complete'),
});
upload({ file, path: 'uploads/my-file.txt' });useSaveFile
Save content to a file:
import { useSaveFile } from '@statelyjs/files/hooks';
const { mutate: save } = useSaveFile();
save({
path: { dir: 'data', path: 'config.json' },
content: JSON.stringify(config, null, 2),
});Views
Reusable view components for building file interfaces:
FileExplorer
Directory browser with navigation:
import { FileExplorer } from '@statelyjs/files/views';
<FileExplorer
path="data/configs"
onNavigate={setCurrentPath}
onSelect={handleFileSelect}
/>FileSelector
File picker dialog:
import { FileSelector } from '@statelyjs/files/views';
<FileSelector
value={selectedPath}
onChange={setSelectedPath}
filter={entry => entry.name.endsWith('.json')}
/>FileDetails / VersionedFileDetails
Display file metadata and actions:
import { FileDetails, VersionedFileDetails } from '@statelyjs/files/views';
// For simple files
<FileDetails entry={fileEntry} currentPath={path} onClose={handleClose} />
// For versioned files (shows version history)
<VersionedFileDetails
entry={fileEntry}
currentPath={path}
onClose={handleClose}
/>FileManager
Full featured file manager, used in FileManagerPage:
import { FileManager } from '@statelyjs/files/views';
<FileManager />Pages
FileManagerPage
Full-featured file management page with:
- Directory navigation breadcrumbs
- File/folder listing with icons
- File details sidebar
- Version history for versioned files
- Download buttons
import { FileManagerPage } from '@statelyjs/files/pages';
<Route path="/files/*" element={<FileManagerPage />} />Codegen Integration
The package includes a codegen plugin that detects RelativePath schemas from your OpenAPI spec. Add it to your stately.config.js:
// stately.config.js
import { filesCodegenPlugin } from '@statelyjs/files/codegen';
export default {
plugins: [filesCodegenPlugin()],
};Then run:
pnpm exec stately ./openapi.json -o ./src/generated -c ./stately.config.jsThis transforms OpenAPI oneOf schemas matching the RelativePath pattern into FilesNodeType.RelativePath nodes, enabling the custom path selector field in forms.
Backend Requirements
This plugin expects a stately-files compatible backend with these endpoints:
| Endpoint | Method | Description |
|----------|--------|-------------|
| /list | GET | List files (query: path) |
| /save | POST | Save file content |
| /upload | POST | Upload file (multipart) |
| /file/cache/{path} | GET | Download from cache |
| /file/data/{path} | GET | Download from data |
| /file/upload/{path} | GET | Download from uploads |
All download endpoints support an optional version query parameter for versioned files.
Plugin Architecture
For plugin authors, this package demonstrates the Stately plugin pattern:
Schema Plugin
Extends the node type system, expected types, additional data properties, and files utilities:
export const FILES_PLUGIN_NAME = 'files' as const;
export type FilesPlugin = DefinePlugin<
typeof FILES_PLUGIN_NAME,
FilesNodeMap,
FilesTypes,
FilesData,
FilesUtils
>;
export function filesPlugin<S extends Schemas<any, any> = Schemas>(): PluginFactory<S> {
return runtime => {
return {
...runtime,
data: { ...runtime.data },
plugins: { ...runtime.plugins, [FILES_PLUGIN_NAME]: {} },
};
};
}UI Plugin
Registers components, navigation, and API bindings:
export type FilesOptions = DefineOptions<{
/** API configuration for Files endpoints */
api?: { pathPrefix?: string };
/** Navigation configuration for Files routes */
navigation?: { routes?: UiNavigationOptions['routes'] };
}>;
export type FilesUiPlugin = DefineUiPlugin<
typeof FILES_PLUGIN_NAME,
FilesPaths,
typeof FILES_OPERATIONS,
FilesUiUtils,
FilesOptions,
typeof filesRoutes
>;
export function filesUiPlugin<
Schema extends Schemas<any, any> = Schemas,
Augments extends readonly AnyUiPlugin[] = [],
>(options?: FilesOptions): UiPluginFactory<Schema, Augments> {
return runtime => {
...
// Register components
registry.components.set(
baseRegistry.makeRegistryKey(FilesNodeType.RelativePath, 'edit'),
props => <RelativePathEdit {...props} standalone />,
);
registry.components.set(
baseRegistry.makeRegistryKey(FilesNodeType.RelativePath, 'view'),
RelativePathView,
);
// Register transformers
registry.transformers.set(
baseRegistry.makeRegistryKey(CoreNodeType.Primitive, 'edit', 'transformer', 'string'),
primitiveStringTransformer,
);
...
const pathPrefix = runtime.utils.mergePathPrefixOptions(basePathPrefix, corePathPrefix);
const api = createOperations<FilesPaths, typeof FILES_OPERATIONS>(
client,
FILES_OPERATIONS,
pathPrefix,
);
// Files only supports a top level route, only provides a single page.
const routes = { ...filesRoutes, ...(options?.navigation?.routes || {}) };
const plugin = { [FILES_PLUGIN_NAME]: { api, options, routes, utils: filesUiUtils } };
return { ...runtime, plugins: { ...runtime.plugins, ...plugin } };
};
}The UI plugin:
- Creates an API client bound to your base URL
- Registers the
RelativePathFieldcomponent for theRelativePathnode type - Provides a prop transformer for core's PrimitiveNode::PrimitiveString fields
- Provides the files context to child components
Context Access
Access the files runtime from components:
import { useFilesStatelyUi } from '@statelyjs/files';
function MyComponent() {
const runtime = useFilesStatelyUi();
const filesApi = runtime.plugins.files?.api;
// Use filesApi for custom operations
}License
Apache-2.0
