@jk3labs/paperclip-plugin-file-browser 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/IMPLEMENTATION_NOTES.md +0 -0
- package/README.md +80 -19
- package/dist/__tests__/file-browser.spec.js +140 -0
- package/dist/index.js +110 -0
- package/index.js +82 -0
- package/jest.config.js +6 -0
- package/manifest.ts +15 -0
- package/package.json +34 -48
- package/src/__tests__/file-browser.spec.ts +161 -0
- package/src/index.ts +128 -0
- package/tsconfig.json +15 -0
- package/dist/manifest.js +0 -89
- package/dist/manifest.js.map +0 -7
- package/dist/ui/index.js +0 -159
- package/dist/ui/index.js.map +0 -7
- package/dist/worker.js +0 -9268
- package/dist/worker.js.map +0 -7
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import request from 'supertest';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
jest.mock('archiver', () => {
|
|
7
|
+
return jest.fn().mockImplementation(() => {
|
|
8
|
+
const { PassThrough } = require('stream');
|
|
9
|
+
const stream = new PassThrough();
|
|
10
|
+
setTimeout(() => { stream.end(); }, 50);
|
|
11
|
+
return {
|
|
12
|
+
pipe: (dest: NodeJS.WritableStream) => { stream.pipe(dest); return dest; },
|
|
13
|
+
directory: () => {},
|
|
14
|
+
finalize: jest.fn().mockResolvedValue(undefined),
|
|
15
|
+
};
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
import plugin from '../index';
|
|
20
|
+
|
|
21
|
+
const mockRootDir = path.join(__dirname, 'mock-root');
|
|
22
|
+
|
|
23
|
+
function createApp(): express.Express {
|
|
24
|
+
const app = express();
|
|
25
|
+
const routers: Array<{ basePath: string; router: express.Router }> = [];
|
|
26
|
+
|
|
27
|
+
const context = {
|
|
28
|
+
addRouter: (basePath: string, router: express.Router) => {
|
|
29
|
+
routers.push({ basePath, router });
|
|
30
|
+
},
|
|
31
|
+
environment: {
|
|
32
|
+
rootDir: mockRootDir,
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
plugin.register(context);
|
|
37
|
+
|
|
38
|
+
for (const { basePath, router } of routers) {
|
|
39
|
+
app.use(basePath, router);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return app;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe('File Browser Plugin', () => {
|
|
46
|
+
let app: express.Express;
|
|
47
|
+
const mockFilePath = path.join(mockRootDir, 'test.txt');
|
|
48
|
+
const mockDirPath = path.join(mockRootDir, 'test-dir');
|
|
49
|
+
const mockNestedFilePath = path.join(mockDirPath, 'nested.txt');
|
|
50
|
+
|
|
51
|
+
beforeAll(async () => {
|
|
52
|
+
await fs.ensureDir(mockRootDir);
|
|
53
|
+
await fs.writeFile(mockFilePath, 'test content');
|
|
54
|
+
await fs.ensureDir(mockDirPath);
|
|
55
|
+
await fs.writeFile(mockNestedFilePath, 'nested content');
|
|
56
|
+
app = createApp();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
afterAll(async () => {
|
|
60
|
+
await fs.remove(mockRootDir);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('Plugin registration', () => {
|
|
64
|
+
it('should throw if rootDir is invalid', () => {
|
|
65
|
+
const badContext = {
|
|
66
|
+
addRouter: () => {},
|
|
67
|
+
environment: { rootDir: '/nonexistent/path/that/does/not/exist' },
|
|
68
|
+
};
|
|
69
|
+
expect(() => plugin.register(badContext)).toThrow('Invalid root directory');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should register a router on /files', () => {
|
|
73
|
+
const registeredPaths: string[] = [];
|
|
74
|
+
const ctx = {
|
|
75
|
+
addRouter: (basePath: string) => { registeredPaths.push(basePath); },
|
|
76
|
+
environment: { rootDir: mockRootDir },
|
|
77
|
+
};
|
|
78
|
+
plugin.register(ctx);
|
|
79
|
+
expect(registeredPaths).toContain('/files');
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('GET /files/list', () => {
|
|
84
|
+
it('should list files and directories in root', async () => {
|
|
85
|
+
const response = await request(app).get('/files/list');
|
|
86
|
+
expect(response.status).toBe(200);
|
|
87
|
+
expect(response.body.files).toContain('test.txt');
|
|
88
|
+
expect(response.body.directories).toContain('test-dir');
|
|
89
|
+
expect(response.body.currentPath).toBe('');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should list files and directories in subdirectory', async () => {
|
|
93
|
+
const response = await request(app).get('/files/list?path=test-dir');
|
|
94
|
+
expect(response.status).toBe(200);
|
|
95
|
+
expect(response.body.files).toContain('nested.txt');
|
|
96
|
+
expect(response.body.directories).toEqual([]);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should return 404 for non-existent directory', async () => {
|
|
100
|
+
const response = await request(app).get('/files/list?path=non-existent');
|
|
101
|
+
expect(response.status).toBe(404);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should return 403 for path traversal attempt', async () => {
|
|
105
|
+
const response = await request(app).get('/files/list?path=../../../etc');
|
|
106
|
+
expect(response.status).toBe(403);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('GET /files/download', () => {
|
|
111
|
+
it('should download a file', async () => {
|
|
112
|
+
const response = await request(app).get('/files/download?path=test.txt');
|
|
113
|
+
expect(response.status).toBe(200);
|
|
114
|
+
expect(response.text).toBe('test content');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should return 400 when path is missing', async () => {
|
|
118
|
+
const response = await request(app).get('/files/download');
|
|
119
|
+
expect(response.status).toBe(400);
|
|
120
|
+
expect(response.body.error).toBe('Path parameter is required');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should return 404 for non-existent file', async () => {
|
|
124
|
+
const response = await request(app).get('/files/download?path=non-existent.txt');
|
|
125
|
+
expect(response.status).toBe(404);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should return 403 for path traversal attempt', async () => {
|
|
129
|
+
const response = await request(app).get('/files/download?path=../../../etc/passwd');
|
|
130
|
+
expect(response.status).toBe(403);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('GET /files/zip', () => {
|
|
135
|
+
it('should return a zip response for a valid directory', async () => {
|
|
136
|
+
const response = await request(app).get('/files/zip?path=test-dir');
|
|
137
|
+
expect(response.status).toBe(200);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should return 400 when path is missing', async () => {
|
|
141
|
+
const response = await request(app).get('/files/zip');
|
|
142
|
+
expect(response.status).toBe(400);
|
|
143
|
+
expect(response.body.error).toBe('Path parameter is required');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should return 404 for non-existent directory', async () => {
|
|
147
|
+
const response = await request(app).get('/files/zip?path=non-existent');
|
|
148
|
+
expect(response.status).toBe(404);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should return 404 when path points to a file', async () => {
|
|
152
|
+
const response = await request(app).get('/files/zip?path=test.txt');
|
|
153
|
+
expect(response.status).toBe(404);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should return 403 for path traversal attempt', async () => {
|
|
157
|
+
const response = await request(app).get('/files/zip?path=../../../etc');
|
|
158
|
+
expect(response.status).toBe(403);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import archiver from 'archiver';
|
|
5
|
+
|
|
6
|
+
interface PluginContext {
|
|
7
|
+
addRouter: (basePath: string, router: express.Router) => void;
|
|
8
|
+
environment: {
|
|
9
|
+
rootDir: string;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const plugin = {
|
|
14
|
+
id: 'file-browser',
|
|
15
|
+
name: 'File Browser Plugin',
|
|
16
|
+
description: 'A plugin to browse, download, and zip files in the Paperclip workspace.',
|
|
17
|
+
register: (context: PluginContext) => {
|
|
18
|
+
const router = express.Router();
|
|
19
|
+
const rootDir = context.environment.rootDir;
|
|
20
|
+
|
|
21
|
+
// Validate rootDir exists
|
|
22
|
+
if (!rootDir || !(fs.existsSync(rootDir))) {
|
|
23
|
+
throw new Error(`Invalid root directory: ${rootDir}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
router.get('/list', async (req, res) => {
|
|
27
|
+
try {
|
|
28
|
+
const relativePath = req.query.path as string || '';
|
|
29
|
+
const targetPath = path.join(rootDir, relativePath);
|
|
30
|
+
|
|
31
|
+
// Security: Ensure targetPath is within rootDir
|
|
32
|
+
if (!targetPath.startsWith(rootDir)) {
|
|
33
|
+
return res.status(403).json({ error: 'Access denied' });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const entries = await fs.readdir(targetPath, { withFileTypes: true });
|
|
37
|
+
const files = entries
|
|
38
|
+
.filter(dirent => dirent.isFile())
|
|
39
|
+
.map(dirent => dirent.name);
|
|
40
|
+
const directories = entries
|
|
41
|
+
.filter(dirent => dirent.isDirectory())
|
|
42
|
+
.map(dirent => dirent.name);
|
|
43
|
+
|
|
44
|
+
res.json({
|
|
45
|
+
files,
|
|
46
|
+
directories,
|
|
47
|
+
currentPath: path.relative(rootDir, targetPath),
|
|
48
|
+
rootDir
|
|
49
|
+
});
|
|
50
|
+
} catch (error) {
|
|
51
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
52
|
+
const statusCode = (error as NodeJS.ErrnoException).code === 'ENOENT' ? 404 : 500;
|
|
53
|
+
res.status(statusCode).json({ error: message });
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
router.get('/download', async (req, res) => {
|
|
58
|
+
try {
|
|
59
|
+
const relativePath = req.query.path as string;
|
|
60
|
+
if (!relativePath) {
|
|
61
|
+
return res.status(400).json({ error: 'Path parameter is required' });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const targetPath = path.join(rootDir, relativePath);
|
|
65
|
+
|
|
66
|
+
// Security: Ensure targetPath is within rootDir
|
|
67
|
+
if (!targetPath.startsWith(rootDir)) {
|
|
68
|
+
return res.status(403).json({ error: 'Access denied' });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Check if path exists and is a file
|
|
72
|
+
const stats = await fs.stat(targetPath);
|
|
73
|
+
if (!stats.isFile()) {
|
|
74
|
+
return res.status(404).json({ error: 'File not found' });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Stream file for download
|
|
78
|
+
res.download(targetPath, path.basename(targetPath));
|
|
79
|
+
} catch (error) {
|
|
80
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
81
|
+
const statusCode = (error as NodeJS.ErrnoException).code === 'ENOENT' ? 404 : 500;
|
|
82
|
+
res.status(statusCode).json({ error: message });
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
router.get('/zip', async (req, res) => {
|
|
87
|
+
try {
|
|
88
|
+
const relativePath = req.query.path as string;
|
|
89
|
+
if (!relativePath) {
|
|
90
|
+
return res.status(400).json({ error: 'Path parameter is required' });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const targetPath = path.join(rootDir, relativePath);
|
|
94
|
+
|
|
95
|
+
// Security: Ensure targetPath is within rootDir
|
|
96
|
+
if (!targetPath.startsWith(rootDir)) {
|
|
97
|
+
return res.status(403).json({ error: 'Access denied' });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Check if path exists and is a directory
|
|
101
|
+
const stats = await fs.stat(targetPath);
|
|
102
|
+
if (!stats.isDirectory()) {
|
|
103
|
+
return res.status(404).json({ error: 'Directory not found' });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Set headers for ZIP download
|
|
107
|
+
res.attachment(`${path.basename(targetPath)}.zip`);
|
|
108
|
+
|
|
109
|
+
// Create ZIP stream
|
|
110
|
+
const archive = archiver('zip', {
|
|
111
|
+
zlib: { level: 9 } // Maximum compression
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
archive.pipe(res);
|
|
115
|
+
archive.directory(targetPath, false);
|
|
116
|
+
await archive.finalize();
|
|
117
|
+
} catch (error) {
|
|
118
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
119
|
+
const statusCode = (error as NodeJS.ErrnoException).code === 'ENOENT' ? 404 : 500;
|
|
120
|
+
res.status(statusCode).json({ error: message });
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
context.addRouter('/files', router);
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export default plugin;
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"outDir": "./dist",
|
|
6
|
+
"rootDir": "./src",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
|
11
|
+
"types": ["jest", "node"]
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*", "src/__tests__/**/*"],
|
|
14
|
+
"exclude": ["node_modules"]
|
|
15
|
+
}
|
package/dist/manifest.js
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
// src/manifest.ts
|
|
2
|
-
var manifest = {
|
|
3
|
-
id: "jkl.file-browser",
|
|
4
|
-
apiVersion: 1,
|
|
5
|
-
version: "0.2.0",
|
|
6
|
-
displayName: "File Browser",
|
|
7
|
-
description: "A file browser plugin for Paperclip workspaces",
|
|
8
|
-
author: "Plugin Author",
|
|
9
|
-
categories: ["workspace"],
|
|
10
|
-
capabilities: [
|
|
11
|
-
"events.subscribe",
|
|
12
|
-
"plugin.state.read",
|
|
13
|
-
"plugin.state.write",
|
|
14
|
-
"local.folders",
|
|
15
|
-
"ui.dashboardWidget.register",
|
|
16
|
-
"ui.detailTab.register",
|
|
17
|
-
"ui.sidebar.register",
|
|
18
|
-
"api.routes.register",
|
|
19
|
-
"project.workspaces.read"
|
|
20
|
-
],
|
|
21
|
-
localFolders: [
|
|
22
|
-
{
|
|
23
|
-
folderKey: "workspace",
|
|
24
|
-
displayName: "Workspace Root",
|
|
25
|
-
description: "Root directory of the project workspace to browse files",
|
|
26
|
-
access: "read"
|
|
27
|
-
}
|
|
28
|
-
],
|
|
29
|
-
apiRoutes: [
|
|
30
|
-
{
|
|
31
|
-
routeKey: "list-files",
|
|
32
|
-
method: "GET",
|
|
33
|
-
path: "/files/list",
|
|
34
|
-
auth: "board-or-agent",
|
|
35
|
-
capability: "api.routes.register",
|
|
36
|
-
companyResolution: { from: "query", key: "companyId" }
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
routeKey: "download-file",
|
|
40
|
-
method: "GET",
|
|
41
|
-
path: "/files/download",
|
|
42
|
-
auth: "board-or-agent",
|
|
43
|
-
capability: "api.routes.register",
|
|
44
|
-
companyResolution: { from: "query", key: "companyId" }
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
routeKey: "download-zip",
|
|
48
|
-
method: "GET",
|
|
49
|
-
path: "/files/zip",
|
|
50
|
-
auth: "board-or-agent",
|
|
51
|
-
capability: "api.routes.register",
|
|
52
|
-
companyResolution: { from: "query", key: "companyId" }
|
|
53
|
-
}
|
|
54
|
-
],
|
|
55
|
-
entrypoints: {
|
|
56
|
-
worker: "./dist/worker.js",
|
|
57
|
-
ui: "./dist/ui"
|
|
58
|
-
},
|
|
59
|
-
ui: {
|
|
60
|
-
slots: [
|
|
61
|
-
{
|
|
62
|
-
type: "dashboardWidget",
|
|
63
|
-
id: "file-browser-widget",
|
|
64
|
-
displayName: "File Browser",
|
|
65
|
-
exportName: "DashboardWidget"
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
type: "detailTab",
|
|
69
|
-
id: "file-browser-tab",
|
|
70
|
-
displayName: "Files",
|
|
71
|
-
exportName: "FileBrowserTab",
|
|
72
|
-
entityTypes: ["project", "issue"],
|
|
73
|
-
order: 50
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
type: "projectSidebarItem",
|
|
77
|
-
id: "file-browser-sidebar",
|
|
78
|
-
displayName: "Files",
|
|
79
|
-
exportName: "FileBrowserSidebarItem",
|
|
80
|
-
entityTypes: ["project"]
|
|
81
|
-
}
|
|
82
|
-
]
|
|
83
|
-
}
|
|
84
|
-
};
|
|
85
|
-
var manifest_default = manifest;
|
|
86
|
-
export {
|
|
87
|
-
manifest_default as default
|
|
88
|
-
};
|
|
89
|
-
//# sourceMappingURL=manifest.js.map
|
package/dist/manifest.js.map
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../src/manifest.ts"],
|
|
4
|
-
"sourcesContent": ["import type { PaperclipPluginManifestV1 } from \"@paperclipai/plugin-sdk\";\n\nconst manifest: PaperclipPluginManifestV1 = {\n id: \"jkl.file-browser\",\n apiVersion: 1,\n version: \"0.2.0\",\n displayName: \"File Browser\",\n description: \"A file browser plugin for Paperclip workspaces\",\n author: \"Plugin Author\",\n categories: [\"workspace\"],\n capabilities: [\n \"events.subscribe\",\n \"plugin.state.read\",\n \"plugin.state.write\",\n \"local.folders\",\n \"ui.dashboardWidget.register\",\n \"ui.detailTab.register\",\n \"ui.sidebar.register\",\n \"api.routes.register\",\n \"project.workspaces.read\"\n ],\n localFolders: [\n {\n folderKey: \"workspace\",\n displayName: \"Workspace Root\",\n description: \"Root directory of the project workspace to browse files\",\n access: \"read\"\n }\n ],\n apiRoutes: [\n {\n routeKey: \"list-files\",\n method: \"GET\",\n path: \"/files/list\",\n auth: \"board-or-agent\",\n capability: \"api.routes.register\",\n companyResolution: { from: \"query\", key: \"companyId\" }\n },\n {\n routeKey: \"download-file\",\n method: \"GET\",\n path: \"/files/download\",\n auth: \"board-or-agent\",\n capability: \"api.routes.register\",\n companyResolution: { from: \"query\", key: \"companyId\" }\n },\n {\n routeKey: \"download-zip\",\n method: \"GET\",\n path: \"/files/zip\",\n auth: \"board-or-agent\",\n capability: \"api.routes.register\",\n companyResolution: { from: \"query\", key: \"companyId\" }\n }\n ],\n entrypoints: {\n worker: \"./dist/worker.js\",\n ui: \"./dist/ui\"\n },\n ui: {\n slots: [\n {\n type: \"dashboardWidget\",\n id: \"file-browser-widget\",\n displayName: \"File Browser\",\n exportName: \"DashboardWidget\"\n },\n {\n type: \"detailTab\",\n id: \"file-browser-tab\",\n displayName: \"Files\",\n exportName: \"FileBrowserTab\",\n entityTypes: [\"project\", \"issue\"],\n order: 50\n },\n {\n type: \"projectSidebarItem\",\n id: \"file-browser-sidebar\",\n displayName: \"Files\",\n exportName: \"FileBrowserSidebarItem\",\n entityTypes: [\"project\"]\n }\n ]\n }\n};\n\nexport default manifest;\n"],
|
|
5
|
-
"mappings": ";AAEA,IAAM,WAAsC;AAAA,EAC1C,IAAI;AAAA,EACJ,YAAY;AAAA,EACZ,SAAS;AAAA,EACT,aAAa;AAAA,EACb,aAAa;AAAA,EACb,QAAQ;AAAA,EACR,YAAY,CAAC,WAAW;AAAA,EACxB,cAAc;AAAA,IACZ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAAA,EACA,cAAc;AAAA,IACZ;AAAA,MACE,WAAW;AAAA,MACX,aAAa;AAAA,MACb,aAAa;AAAA,MACb,QAAQ;AAAA,IACV;AAAA,EACF;AAAA,EACA,WAAW;AAAA,IACT;AAAA,MACE,UAAU;AAAA,MACV,QAAQ;AAAA,MACR,MAAM;AAAA,MACN,MAAM;AAAA,MACN,YAAY;AAAA,MACZ,mBAAmB,EAAE,MAAM,SAAS,KAAK,YAAY;AAAA,IACvD;AAAA,IACA;AAAA,MACE,UAAU;AAAA,MACV,QAAQ;AAAA,MACR,MAAM;AAAA,MACN,MAAM;AAAA,MACN,YAAY;AAAA,MACZ,mBAAmB,EAAE,MAAM,SAAS,KAAK,YAAY;AAAA,IACvD;AAAA,IACA;AAAA,MACE,UAAU;AAAA,MACV,QAAQ;AAAA,MACR,MAAM;AAAA,MACN,MAAM;AAAA,MACN,YAAY;AAAA,MACZ,mBAAmB,EAAE,MAAM,SAAS,KAAK,YAAY;AAAA,IACvD;AAAA,EACF;AAAA,EACA,aAAa;AAAA,IACX,QAAQ;AAAA,IACR,IAAI;AAAA,EACN;AAAA,EACA,IAAI;AAAA,IACF,OAAO;AAAA,MACL;AAAA,QACE,MAAM;AAAA,QACN,IAAI;AAAA,QACJ,aAAa;AAAA,QACb,YAAY;AAAA,MACd;AAAA,MACA;AAAA,QACE,MAAM;AAAA,QACN,IAAI;AAAA,QACJ,aAAa;AAAA,QACb,YAAY;AAAA,QACZ,aAAa,CAAC,WAAW,OAAO;AAAA,QAChC,OAAO;AAAA,MACT;AAAA,MACA;AAAA,QACE,MAAM;AAAA,QACN,IAAI;AAAA,QACJ,aAAa;AAAA,QACb,YAAY;AAAA,QACZ,aAAa,CAAC,SAAS;AAAA,MACzB;AAAA,IACF;AAAA,EACF;AACF;AAEA,IAAO,mBAAQ;",
|
|
6
|
-
"names": []
|
|
7
|
-
}
|
package/dist/ui/index.js
DELETED
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
// src/ui/index.tsx
|
|
2
|
-
import { useCallback, useState } from "react";
|
|
3
|
-
import {
|
|
4
|
-
usePluginData,
|
|
5
|
-
usePluginAction,
|
|
6
|
-
useHostContext,
|
|
7
|
-
useHostNavigation
|
|
8
|
-
} from "@paperclipai/plugin-sdk/ui";
|
|
9
|
-
import {
|
|
10
|
-
FileTree,
|
|
11
|
-
MetricCard,
|
|
12
|
-
ErrorBoundary
|
|
13
|
-
} from "@paperclipai/plugin-sdk/ui";
|
|
14
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
15
|
-
function buildNodes(data) {
|
|
16
|
-
if (!data) return [];
|
|
17
|
-
const dirNodes = data.directories.map((d) => ({
|
|
18
|
-
name: d.name,
|
|
19
|
-
path: d.path,
|
|
20
|
-
kind: "dir",
|
|
21
|
-
children: []
|
|
22
|
-
}));
|
|
23
|
-
const fileNodes = data.files.map((f) => ({
|
|
24
|
-
name: f.name,
|
|
25
|
-
path: f.path,
|
|
26
|
-
kind: "file",
|
|
27
|
-
children: []
|
|
28
|
-
}));
|
|
29
|
-
return [...dirNodes, ...fileNodes];
|
|
30
|
-
}
|
|
31
|
-
function formatSize(bytes) {
|
|
32
|
-
if (bytes == null) return "-";
|
|
33
|
-
if (bytes < 1024) return `${bytes} B`;
|
|
34
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
35
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
36
|
-
}
|
|
37
|
-
function FileBrowser({ companyId }) {
|
|
38
|
-
const [currentPath, setCurrentPath] = useState("");
|
|
39
|
-
const [selectedFile, setSelectedFile] = useState(null);
|
|
40
|
-
const { data, loading, error, refresh } = usePluginData(
|
|
41
|
-
"file-tree",
|
|
42
|
-
{ companyId: companyId ?? "", relativePath: currentPath }
|
|
43
|
-
);
|
|
44
|
-
const downloadFile = usePluginAction("download-file");
|
|
45
|
-
const downloadZip = usePluginAction("download-zip");
|
|
46
|
-
const nodes = buildNodes(data);
|
|
47
|
-
const handleSelectFile = useCallback((path) => {
|
|
48
|
-
setSelectedFile(path);
|
|
49
|
-
}, []);
|
|
50
|
-
const handleToggleDir = useCallback((path) => {
|
|
51
|
-
setCurrentPath(path);
|
|
52
|
-
setSelectedFile(null);
|
|
53
|
-
}, []);
|
|
54
|
-
const handleDownloadFile = useCallback(async () => {
|
|
55
|
-
if (!selectedFile || !companyId) return;
|
|
56
|
-
try {
|
|
57
|
-
await downloadFile({ companyId, relativePath: selectedFile });
|
|
58
|
-
} catch {
|
|
59
|
-
}
|
|
60
|
-
}, [selectedFile, companyId, downloadFile]);
|
|
61
|
-
const handleDownloadZip = useCallback(async () => {
|
|
62
|
-
if (!currentPath || !companyId) return;
|
|
63
|
-
try {
|
|
64
|
-
await downloadZip({ companyId, relativePath: currentPath });
|
|
65
|
-
} catch {
|
|
66
|
-
}
|
|
67
|
-
}, [currentPath, companyId, downloadZip]);
|
|
68
|
-
const handleNavigateUp = useCallback(() => {
|
|
69
|
-
if (!currentPath) return;
|
|
70
|
-
const parts = currentPath.split("/");
|
|
71
|
-
parts.pop();
|
|
72
|
-
setCurrentPath(parts.join("/"));
|
|
73
|
-
setSelectedFile(null);
|
|
74
|
-
}, [currentPath]);
|
|
75
|
-
const handleRefresh = useCallback(() => {
|
|
76
|
-
refresh();
|
|
77
|
-
}, [refresh]);
|
|
78
|
-
const fileCount = data?.files.length ?? 0;
|
|
79
|
-
const dirCount = data?.directories.length ?? 0;
|
|
80
|
-
const totalSize = data?.files.reduce((sum, f) => sum + (f.size ?? 0), 0) ?? 0;
|
|
81
|
-
if (!companyId) {
|
|
82
|
-
return /* @__PURE__ */ jsx("div", { style: { padding: "1rem" }, children: "Select a company to browse files." });
|
|
83
|
-
}
|
|
84
|
-
return /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "0.75rem", padding: "0.5rem 0" }, children: [
|
|
85
|
-
/* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: "0.5rem", flexWrap: "wrap" }, children: [
|
|
86
|
-
/* @__PURE__ */ jsx(MetricCard, { label: "Files", value: fileCount }),
|
|
87
|
-
/* @__PURE__ */ jsx(MetricCard, { label: "Directories", value: dirCount }),
|
|
88
|
-
/* @__PURE__ */ jsx(MetricCard, { label: "Total Size", value: formatSize(totalSize) })
|
|
89
|
-
] }),
|
|
90
|
-
/* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: "0.5rem", alignItems: "center", flexWrap: "wrap" }, children: [
|
|
91
|
-
currentPath && /* @__PURE__ */ jsx("button", { onClick: handleNavigateUp, style: { fontSize: "0.85rem" }, children: ".. /" }),
|
|
92
|
-
/* @__PURE__ */ jsxs("span", { style: { fontSize: "0.85rem", color: "var(--text-secondary, #888)" }, children: [
|
|
93
|
-
"/",
|
|
94
|
-
currentPath || ""
|
|
95
|
-
] }),
|
|
96
|
-
/* @__PURE__ */ jsxs("div", { style: { marginLeft: "auto", display: "flex", gap: "0.5rem" }, children: [
|
|
97
|
-
/* @__PURE__ */ jsx("button", { onClick: handleRefresh, style: { fontSize: "0.85rem" }, children: "Refresh" }),
|
|
98
|
-
selectedFile && /* @__PURE__ */ jsx("button", { onClick: handleDownloadFile, style: { fontSize: "0.85rem" }, children: "Download File" }),
|
|
99
|
-
currentPath && /* @__PURE__ */ jsx("button", { onClick: handleDownloadZip, style: { fontSize: "0.85rem" }, children: "Download Folder (ZIP)" })
|
|
100
|
-
] })
|
|
101
|
-
] }),
|
|
102
|
-
data?.error && /* @__PURE__ */ jsxs("div", { style: { color: "var(--text-error, red)", fontSize: "0.85rem" }, children: [
|
|
103
|
-
"Error: ",
|
|
104
|
-
data.error
|
|
105
|
-
] }),
|
|
106
|
-
/* @__PURE__ */ jsx(
|
|
107
|
-
FileTree,
|
|
108
|
-
{
|
|
109
|
-
nodes,
|
|
110
|
-
selectedFile,
|
|
111
|
-
expandedPaths: [],
|
|
112
|
-
onSelectFile: handleSelectFile,
|
|
113
|
-
onToggleDir: handleToggleDir,
|
|
114
|
-
loading,
|
|
115
|
-
error: error ? { message: error.message } : null,
|
|
116
|
-
empty: { title: "Empty directory", description: "No files or directories found." },
|
|
117
|
-
ariaLabel: "File browser"
|
|
118
|
-
}
|
|
119
|
-
),
|
|
120
|
-
selectedFile && /* @__PURE__ */ jsxs("div", { style: { fontSize: "0.8rem", color: "var(--text-secondary, #888)", paddingTop: "0.25rem" }, children: [
|
|
121
|
-
"Selected: ",
|
|
122
|
-
selectedFile
|
|
123
|
-
] })
|
|
124
|
-
] });
|
|
125
|
-
}
|
|
126
|
-
function DashboardWidget(_props) {
|
|
127
|
-
const { companyId } = useHostContext();
|
|
128
|
-
return /* @__PURE__ */ jsx(ErrorBoundary, { fallback: /* @__PURE__ */ jsx("div", { style: { padding: "1rem", color: "var(--text-error, red)" }, children: "File Browser: failed to render" }), children: /* @__PURE__ */ jsxs("div", { style: { display: "grid", gap: "0.5rem" }, children: [
|
|
129
|
-
/* @__PURE__ */ jsx("strong", { children: "File Browser" }),
|
|
130
|
-
/* @__PURE__ */ jsx(FileBrowser, { companyId })
|
|
131
|
-
] }) });
|
|
132
|
-
}
|
|
133
|
-
function FileBrowserTab(props) {
|
|
134
|
-
const { companyId } = props.context;
|
|
135
|
-
return /* @__PURE__ */ jsx(ErrorBoundary, { fallback: /* @__PURE__ */ jsx("div", { style: { padding: "1rem", color: "var(--text-error, red)" }, children: "File Browser tab: failed to render" }), children: /* @__PURE__ */ jsx(FileBrowser, { companyId }) });
|
|
136
|
-
}
|
|
137
|
-
function FileBrowserSidebarItem(props) {
|
|
138
|
-
const { entityId: projectId } = props.context;
|
|
139
|
-
const { navigate, resolveHref } = useHostNavigation();
|
|
140
|
-
const href = resolveHref(`/projects/${projectId}?tab=plugin:jkl.file-browser:file-browser-tab`);
|
|
141
|
-
return /* @__PURE__ */ jsx(
|
|
142
|
-
"a",
|
|
143
|
-
{
|
|
144
|
-
href,
|
|
145
|
-
onClick: (e) => {
|
|
146
|
-
e.preventDefault();
|
|
147
|
-
navigate(href);
|
|
148
|
-
},
|
|
149
|
-
style: { fontSize: "0.85rem", display: "flex", alignItems: "center", gap: "0.35rem", padding: "0.2rem 0", color: "var(--text-link, #4488cc)", textDecoration: "none" },
|
|
150
|
-
children: "Files"
|
|
151
|
-
}
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
|
-
export {
|
|
155
|
-
DashboardWidget,
|
|
156
|
-
FileBrowserSidebarItem,
|
|
157
|
-
FileBrowserTab
|
|
158
|
-
};
|
|
159
|
-
//# sourceMappingURL=index.js.map
|
package/dist/ui/index.js.map
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../../src/ui/index.tsx"],
|
|
4
|
-
"sourcesContent": ["import { useCallback, useState } from \"react\";\nimport {\n usePluginData,\n usePluginAction,\n useHostContext,\n useHostNavigation,\n type PluginWidgetProps,\n type PluginDetailTabProps,\n type PluginProjectSidebarItemProps,\n} from \"@paperclipai/plugin-sdk/ui\";\nimport {\n FileTree,\n MetricCard,\n Spinner,\n ErrorBoundary,\n type FileTreeNode,\n} from \"@paperclipai/plugin-sdk/ui\";\n\ninterface FileEntry {\n name: string;\n path: string;\n kind: \"file\" | \"directory\";\n size?: number | null;\n modifiedAt?: string | null;\n}\n\ninterface FileTreeData {\n currentPath: string;\n directories: FileEntry[];\n files: FileEntry[];\n error?: string;\n}\n\nfunction buildNodes(data: FileTreeData | null): FileTreeNode[] {\n if (!data) return [];\n const dirNodes: FileTreeNode[] = data.directories.map((d) => ({\n name: d.name,\n path: d.path,\n kind: \"dir\" as const,\n children: [],\n }));\n const fileNodes: FileTreeNode[] = data.files.map((f) => ({\n name: f.name,\n path: f.path,\n kind: \"file\" as const,\n children: [],\n }));\n return [...dirNodes, ...fileNodes];\n}\n\nfunction formatSize(bytes: number | null | undefined): string {\n if (bytes == null) return \"-\";\n if (bytes < 1024) return `${bytes} B`;\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n}\n\nfunction FileBrowser({ companyId }: { companyId: string | null }) {\n const [currentPath, setCurrentPath] = useState(\"\");\n const [selectedFile, setSelectedFile] = useState<string | null>(null);\n\n const { data, loading, error, refresh } = usePluginData<FileTreeData>(\n \"file-tree\",\n { companyId: companyId ?? \"\", relativePath: currentPath }\n );\n\n const downloadFile = usePluginAction(\"download-file\");\n const downloadZip = usePluginAction(\"download-zip\");\n\n const nodes = buildNodes(data);\n\n const handleSelectFile = useCallback((path: string) => {\n setSelectedFile(path);\n }, []);\n\n const handleToggleDir = useCallback((path: string) => {\n setCurrentPath(path);\n setSelectedFile(null);\n }, []);\n\n const handleDownloadFile = useCallback(async () => {\n if (!selectedFile || !companyId) return;\n try {\n await downloadFile({ companyId, relativePath: selectedFile });\n } catch {\n // Bridge error propagated\n }\n }, [selectedFile, companyId, downloadFile]);\n\n const handleDownloadZip = useCallback(async () => {\n if (!currentPath || !companyId) return;\n try {\n await downloadZip({ companyId, relativePath: currentPath });\n } catch {\n // Bridge error propagated\n }\n }, [currentPath, companyId, downloadZip]);\n\n const handleNavigateUp = useCallback(() => {\n if (!currentPath) return;\n const parts = currentPath.split(\"/\");\n parts.pop();\n setCurrentPath(parts.join(\"/\"));\n setSelectedFile(null);\n }, [currentPath]);\n\n const handleRefresh = useCallback(() => {\n refresh();\n }, [refresh]);\n\n const fileCount = data?.files.length ?? 0;\n const dirCount = data?.directories.length ?? 0;\n const totalSize = data?.files.reduce((sum, f) => sum + (f.size ?? 0), 0) ?? 0;\n\n if (!companyId) {\n return <div style={{ padding: \"1rem\" }}>Select a company to browse files.</div>;\n }\n\n return (\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: \"0.75rem\", padding: \"0.5rem 0\" }}>\n <div style={{ display: \"flex\", gap: \"0.5rem\", flexWrap: \"wrap\" }}>\n <MetricCard label=\"Files\" value={fileCount} />\n <MetricCard label=\"Directories\" value={dirCount} />\n <MetricCard label=\"Total Size\" value={formatSize(totalSize)} />\n </div>\n\n <div style={{ display: \"flex\", gap: \"0.5rem\", alignItems: \"center\", flexWrap: \"wrap\" }}>\n {currentPath && (\n <button onClick={handleNavigateUp} style={{ fontSize: \"0.85rem\" }}>\n .. / \n </button>\n )}\n <span style={{ fontSize: \"0.85rem\", color: \"var(--text-secondary, #888)\" }}>\n /{currentPath || \"\"}\n </span>\n <div style={{ marginLeft: \"auto\", display: \"flex\", gap: \"0.5rem\" }}>\n <button onClick={handleRefresh} style={{ fontSize: \"0.85rem\" }}>\n Refresh\n </button>\n {selectedFile && (\n <button onClick={handleDownloadFile} style={{ fontSize: \"0.85rem\" }}>\n Download File\n </button>\n )}\n {currentPath && (\n <button onClick={handleDownloadZip} style={{ fontSize: \"0.85rem\" }}>\n Download Folder (ZIP)\n </button>\n )}\n </div>\n </div>\n\n {data?.error && (\n <div style={{ color: \"var(--text-error, red)\", fontSize: \"0.85rem\" }}>\n Error: {data.error}\n </div>\n )}\n\n <FileTree\n nodes={nodes}\n selectedFile={selectedFile}\n expandedPaths={[]}\n onSelectFile={handleSelectFile}\n onToggleDir={handleToggleDir}\n loading={loading}\n error={error ? { message: error.message } : null}\n empty={{ title: \"Empty directory\", description: \"No files or directories found.\" }}\n ariaLabel=\"File browser\"\n />\n\n {selectedFile && (\n <div style={{ fontSize: \"0.8rem\", color: \"var(--text-secondary, #888)\", paddingTop: \"0.25rem\" }}>\n Selected: {selectedFile}\n </div>\n )}\n </div>\n );\n}\n\nexport function DashboardWidget(_props: PluginWidgetProps) {\n const { companyId } = useHostContext();\n return (\n <ErrorBoundary fallback={<div style={{ padding: \"1rem\", color: \"var(--text-error, red)\" }}>File Browser: failed to render</div>}>\n <div style={{ display: \"grid\", gap: \"0.5rem\" }}>\n <strong>File Browser</strong>\n <FileBrowser companyId={companyId} />\n </div>\n </ErrorBoundary>\n );\n}\n\nexport function FileBrowserTab(props: PluginDetailTabProps) {\n const { companyId } = props.context;\n return (\n <ErrorBoundary fallback={<div style={{ padding: \"1rem\", color: \"var(--text-error, red)\" }}>File Browser tab: failed to render</div>}>\n <FileBrowser companyId={companyId} />\n </ErrorBoundary>\n );\n}\n\nexport function FileBrowserSidebarItem(props: PluginProjectSidebarItemProps) {\n const { entityId: projectId } = props.context;\n const { navigate, resolveHref } = useHostNavigation();\n const href = resolveHref(`/projects/${projectId}?tab=plugin:jkl.file-browser:file-browser-tab`);\n return (\n <a\n href={href}\n onClick={(e) => { e.preventDefault(); navigate(href); }}\n style={{ fontSize: \"0.85rem\", display: \"flex\", alignItems: \"center\", gap: \"0.35rem\", padding: \"0.2rem 0\", color: \"var(--text-link, #4488cc)\", textDecoration: \"none\" }}\n >\n Files\n </a>\n );\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
6
|
-
"names": []
|
|
7
|
-
}
|